web: test coverage++, adjust commandbar

This commit is contained in:
Maximilian Hils 2021-08-20 18:38:22 +02:00
parent 2945ba925b
commit 46cd40f493
26 changed files with 61445 additions and 186 deletions

View File

@ -529,11 +529,17 @@ class ExecuteCommand(RequestHandler):
args = self.json['arguments']
except APIError:
args = []
result = self.master.commands.call_strings(cmd, args)
self.write({
"value": result,
# "type": command.typename(type(result)) if result is not None else "none"
})
try:
result = self.master.commands.call_strings(cmd, args)
except Exception as e:
self.write({
"error": str(e)
})
else:
self.write({
"value": result,
# "type": command.typename(type(result)) if result is not None else "none"
})
class Events(RequestHandler):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,8 +13,9 @@ module.exports = async () => {
],
"coverageDirectory": "./coverage",
"coveragePathIgnorePatterns": [
"<rootDir>/src/js/filt/filt.js",
"<rootDir>/src/js/filt/command.js"
"<rootDir>/src/js/contrib/",
"<rootDir>/src/js/filt/",
"<rootDir>/src/js/components/editors/"
],
"collectCoverageFrom": [
"src/js/**/*.{js,jsx,ts,tsx}"

152
web/package-lock.json generated
View File

@ -662,12 +662,12 @@
}
},
"@babel/runtime-corejs3": {
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz",
"integrity": "sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==",
"version": "7.15.3",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz",
"integrity": "sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==",
"dev": true,
"requires": {
"core-js-pure": "^3.14.0",
"core-js-pure": "^3.16.0",
"regenerator-runtime": "^0.13.4"
}
},
@ -1025,9 +1025,9 @@
}
},
"@testing-library/dom": {
"version": "7.31.2",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz",
"integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.1.0.tgz",
"integrity": "sha512-kmW9alndr19qd6DABzQ978zKQ+J65gU2Rzkl8hriIetPnwpesRaK4//jEQyYh8fEALmGhomD/LBQqt+o+DL95Q==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
@ -1037,7 +1037,71 @@
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.6",
"lz-string": "^1.4.4",
"pretty-format": "^26.6.2"
"pretty-format": "^27.0.2"
}
},
"@testing-library/jest-dom": {
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz",
"integrity": "sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ==",
"dev": true,
"requires": {
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^4.2.2",
"chalk": "^3.0.0",
"css": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
"dependencies": {
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true
},
"redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"requires": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
}
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"requires": {
"min-indent": "^1.0.0"
}
}
}
},
"@testing-library/react": {
"version": "11.2.7",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz",
"integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^7.28.1"
},
"dependencies": {
"@jest/types": {
@ -1053,10 +1117,26 @@
"chalk": "^4.0.0"
}
},
"@testing-library/dom": {
"version": "7.31.2",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz",
"integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^4.2.0",
"aria-query": "^4.2.2",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.6",
"lz-string": "^1.4.4",
"pretty-format": "^26.6.2"
}
},
"@types/yargs": {
"version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
"integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
"version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
"integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
@ -1076,14 +1156,13 @@
}
}
},
"@testing-library/react": {
"version": "11.2.7",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz",
"integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==",
"@testing-library/user-event": {
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.2.1.tgz",
"integrity": "sha512-cczlgVl+krjOb3j1625usarNEibI0IFRJrSWX9UsJ1HKYFgCQv9Nb7QAipUDXl3Xdz8NDTsiS78eAkPSxlzTlw==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^7.28.1"
"@babel/runtime": "^7.12.5"
}
},
"@tootallnate/once": {
@ -1093,9 +1172,9 @@
"dev": true
},
"@types/aria-query": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz",
"integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
"dev": true
},
"@types/babel__core": {
@ -1336,6 +1415,15 @@
"integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
"dev": true
},
"@types/testing-library__jest-dom": {
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz",
"integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==",
"dev": true,
"requires": {
"@types/jest": "*"
}
},
"@types/vinyl": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.4.tgz",
@ -2652,9 +2740,9 @@
}
},
"core-js-pure": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.0.tgz",
"integrity": "sha512-RO+LFAso8DB6OeBX9BAcEGvyth36QtxYon1OyVsITNVtSKr/Hos0BXZwnsOJ7o+O6KHtK+O+cJIEj9NGg6VwFA==",
"version": "3.16.2",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.2.tgz",
"integrity": "sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==",
"dev": true
},
"core-util-is": {
@ -2729,6 +2817,12 @@
}
}
},
"css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
"dev": true
},
"cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -2954,9 +3048,9 @@
"dev": true
},
"dom-accessibility-api": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz",
"integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==",
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz",
"integrity": "sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==",
"dev": true
},
"domexception": {
@ -6666,6 +6760,12 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",

View File

@ -23,7 +23,10 @@
"stable": "^0.1.8"
},
"devDependencies": {
"@testing-library/dom": "^8.1.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^26.0.23",
"@types/redux-mock-store": "^1.0.2",
"esbuild": "^0.12.9",

View File

@ -1,26 +1,67 @@
import * as React from "react"
import {render, waitFor, screen} from "../test-utils";
import {render, screen, userEvent, waitFor} from "../test-utils";
import CommandBar from "../../components/CommandBar";
import fetchMock, {enableFetchMocks} from "jest-fetch-mock";
enableFetchMocks();
test("CommandBar", async () => {
fetchMock.mockResponseOnce(JSON.stringify({
"flow.decode": {"help": "Decode flows.",
"parameters": [{"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"}, {
"name": "part",
"type": "str",
"kind": "POSITIONAL_OR_KEYWORD"
}],
fetchMock.mockOnceIf("./commands", JSON.stringify({
"flow.decode": {
"help": "Decode flows.",
"parameters": [
{"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"},
{"name": "part", "type": "str", "kind": "POSITIONAL_OR_KEYWORD"}
],
"return_type": null,
"signature_help": "flow.decode flows part"
},
"flow.encode": {
"help": "Encode flows with a specified encoding.",
"parameters": [
{"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"},
{"name": "part", "type": "str", "kind": "POSITIONAL_OR_KEYWORD"},
{"name": "encoding", "type": "choice", "kind": "POSITIONAL_OR_KEYWORD"}
],
"return_type": null,
"signature_help": "flow.encode flows part encoding"
}
}
));
fetchMock.mockOnceIf("./commands/commands.history.get", JSON.stringify({value: ["foo"]}));
fetchMock.mockOnceIf("./commands/commands.history.add", JSON.stringify({value: null}));
fetchMock.mockOnceIf("./commands/flow.encode", JSON.stringify({value: null}));
const {asFragment} = render(<CommandBar/>);
expect(asFragment()).toMatchSnapshot();
await waitFor(() => screen.getByText('["flow.decode"]'))
await waitFor(() => screen.getByText('["flow.decode","flow.encode"]'))
expect(asFragment()).toMatchSnapshot();
const input = screen.getByPlaceholderText("Enter command");
userEvent.type(input, 'x');
expect(screen.getByText("[]")).toBeInTheDocument();
userEvent.type(input, "{backspace}");
userEvent.type(input, 'fl');
userEvent.tab();
expect(input).toHaveValue('flow.decode');
userEvent.tab();
expect(input).toHaveValue('flow.encode');
fetchMock.mockOnce(JSON.stringify({value: null}));
userEvent.type(input, "{enter}");
await waitFor(() => screen.getByText("Command Result"));
userEvent.type(input, "{arrowdown}");
expect(input).toHaveValue("");
userEvent.type(input, "{arrowup}");
expect(input).toHaveValue("flow.encode");
userEvent.type(input, "{arrowup}");
expect(input).toHaveValue("foo");
userEvent.type(input, "{arrowdown}");
expect(input).toHaveValue("flow.encode");
userEvent.type(input, "{arrowdown}");
expect(input).toHaveValue("");
});

View File

@ -109,7 +109,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
>
<label>
<input
checked={true}
checked={false}
onChange={[Function]}
type="checkbox"
/>

View File

@ -0,0 +1,22 @@
import * as React from "react"
import {render, screen} from "../test-utils";
import Header from "../../components/Header";
import {fireEvent} from "@testing-library/react";
test("Header", async () => {
const {asFragment} = render(<Header/>);
expect(asFragment()).toMatchSnapshot();
fireEvent.click(screen.getByText("Options"));
expect(asFragment()).toMatchSnapshot();
expect(screen.getByText("Edit Options")).toBeTruthy();
fireEvent.click(screen.getByText("File"));
expect(asFragment()).toMatchSnapshot();
expect(screen.getByText("Open...")).toBeTruthy();
fireEvent.click(screen.getByText("File"));
expect(screen.queryByText("Open...")).toBeNull()
});

View File

@ -0,0 +1,15 @@
import * as React from "react"
import {render, screen, waitFor} from "../test-utils";
import ProxyApp from "../../components/ProxyApp";
import {enableFetchMocks} from "jest-fetch-mock";
import {ContentViewData} from "../../components/contentviews/useContent";
enableFetchMocks();
test("ProxyApp", async () => {
const cv: ContentViewData = {lines: [[["text", "my data"]]], description: ""}
fetchMock.doMockOnceIf("./flows/flow2/request/content/Auto.json?lines=81", JSON.stringify(cv));
render(<ProxyApp/>);
expect(screen.getByTitle("Mitmproxy Version")).toBeDefined();
await waitFor(() => screen.getByText("my data"));
});

View File

@ -84,7 +84,7 @@ exports[`CommandBar 2`] = `
<p
class="available-commands"
>
["flow.decode"]
["flow.decode","flow.encode"]
</p>
</div>
</div>

View File

@ -0,0 +1,533 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header 1`] = `
<DocumentFragment>
<header>
<nav
class="nav-tabs nav-tabs-lg"
>
<a
class="pull-left special"
href="#"
>
File
</a>
<a
class=""
href="#"
>
Start
</a>
<a
class=""
href="#"
>
Options
</a>
<a
class="active"
href="#"
>
Flow
</a>
<span
class="connection-indicator established"
>
connected
</span>
</nav>
<div>
<div
class="flow-menu"
>
<div
class="menu-group"
>
<div
class="menu-content"
>
<button
class="btn btn-default"
disabled=""
title="[r]eplay flow"
>
<i
class="fa fa-repeat text-primary"
/>
 Replay
</button>
<button
class="btn btn-default"
title="[D]uplicate flow"
>
<i
class="fa fa-copy text-info"
/>
 Duplicate
</button>
<button
class="btn btn-default"
disabled=""
title="revert changes to flow [V]"
>
<i
class="fa fa-history text-warning"
/>
 Revert
</button>
<button
class="btn btn-default"
title="[d]elete flow"
>
<i
class="fa fa-trash text-danger"
/>
 Delete
</button>
</div>
<div
class="menu-legend"
>
Flow Modification
</div>
</div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<a
class=""
href="#"
>
<button
class="btn btn-default"
>
<i
class="fa fa-download"
/>
 Download▾
</button>
</a>
<a
class=""
href="#"
>
<button
class="btn btn-default"
title="Export flow."
>
<i
class="fa fa-clone"
/>
 Export▾
</button>
</a>
</div>
<div
class="menu-legend"
>
Export
</div>
</div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<button
class="btn btn-default"
disabled=""
title="[a]ccept intercepted flow"
>
<i
class="fa fa-play text-success"
/>
 Resume
</button>
<button
class="btn btn-default"
disabled=""
title="kill intercepted flow [x]"
>
<i
class="fa fa-times text-danger"
/>
 Abort
</button>
</div>
<div
class="menu-legend"
>
Interception
</div>
</div>
</div>
</div>
</header>
</DocumentFragment>
`;
exports[`Header 2`] = `
<DocumentFragment>
<header>
<nav
class="nav-tabs nav-tabs-lg"
>
<a
class="pull-left special"
href="#"
>
File
</a>
<a
class=""
href="#"
>
Start
</a>
<a
class="active"
href="#"
>
Options
</a>
<a
class=""
href="#"
>
Flow
</a>
<span
class="connection-indicator established"
>
connected
</span>
</nav>
<div>
<div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<button
class="btn btn-default"
title="Open Options"
>
<i
class="fa fa-cogs text-primary"
/>
 Edit Options
<sup>
alpha
</sup>
</button>
</div>
<div
class="menu-legend"
>
Options Editor
</div>
</div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Strip cache headers
<a
href="https://docs.mitmproxy.org/stable/overview-features/#anticache"
target="_blank"
>
<i
class="fa fa-question-circle"
/>
</a>
</label>
</div>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Use host header for display
</label>
</div>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Don't verify server certificates
</label>
</div>
</div>
<div
class="menu-legend"
>
Quick Options
</div>
</div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<div
class="menu-entry"
>
<label>
<input
checked=""
type="checkbox"
/>
Display Event Log
</label>
</div>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Display Command Bar
</label>
</div>
</div>
<div
class="menu-legend"
>
View Options
</div>
</div>
</div>
</div>
</header>
</DocumentFragment>
`;
exports[`Header 3`] = `
<DocumentFragment>
<header>
<nav
class="nav-tabs nav-tabs-lg"
>
<a
class="pull-left special open"
href="#"
>
File
</a>
<ul
class="dropdown-menu show"
style="position: absolute; left: 0px; top: 0px;"
>
<li>
<a
href="#"
>
<i
class="fa fa-fw fa-folder-open"
/>
 Open...
<input
class="hidden"
type="file"
/>
</a>
</li>
<li>
<a
href="#"
>
<i
class="fa fa-fw fa-floppy-o"
/>
 Save...
</a>
</li>
<li>
<a
href="#"
>
<i
class="fa fa-fw fa-trash"
/>
 Clear All
</a>
</li>
<li
class="divider"
role="separator"
/>
<li>
<a
href="http://mitm.it/"
target="_blank"
>
<i
class="fa fa-fw fa-external-link"
/>
 Install Certificates...
</a>
</li>
</ul>
<a
class=""
href="#"
>
Start
</a>
<a
class="active"
href="#"
>
Options
</a>
<a
class=""
href="#"
>
Flow
</a>
<span
class="connection-indicator established"
>
connected
</span>
</nav>
<div>
<div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<button
class="btn btn-default"
title="Open Options"
>
<i
class="fa fa-cogs text-primary"
/>
 Edit Options
<sup>
alpha
</sup>
</button>
</div>
<div
class="menu-legend"
>
Options Editor
</div>
</div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Strip cache headers
<a
href="https://docs.mitmproxy.org/stable/overview-features/#anticache"
target="_blank"
>
<i
class="fa fa-question-circle"
/>
</a>
</label>
</div>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Use host header for display
</label>
</div>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Don't verify server certificates
</label>
</div>
</div>
<div
class="menu-legend"
>
Quick Options
</div>
</div>
<div
class="menu-group"
>
<div
class="menu-content"
>
<div
class="menu-entry"
>
<label>
<input
checked=""
type="checkbox"
/>
Display Event Log
</label>
</div>
<div
class="menu-entry"
>
<label>
<input
type="checkbox"
/>
Display Command Bar
</label>
</div>
</div>
<div
class="menu-legend"
>
View Options
</div>
</div>
</div>
</div>
</header>
</DocumentFragment>
`;

View File

@ -1,6 +1,6 @@
import * as React from "react"
import ValidateEditor from '../../../components/editors/ValidateEditor'
import {fireEvent, render, screen, waitFor} from "../../test-utils";
import {fireEvent, render, screen, userEvent, waitFor} from "../../test-utils";
test("ValidateEditor", async () => {
const onEditDone = jest.fn();
@ -9,8 +9,7 @@ test("ValidateEditor", async () => {
);
expect(asFragment()).toMatchSnapshot();
fireEvent.mouseDown(screen.getByText("ok"));
fireEvent.mouseUp(screen.getByText("ok"));
userEvent.click(screen.getByText("ok"));
screen.getByText("ok").innerHTML = "this is ok";
@ -19,8 +18,7 @@ test("ValidateEditor", async () => {
await waitFor(() => expect(onEditDone).toBeCalledWith("this is ok"));
onEditDone.mockClear();
fireEvent.mouseDown(screen.getByText("this is ok"));
fireEvent.mouseUp(screen.getByText("this is ok"));
userEvent.click(screen.getByText("this is ok"));
screen.getByText("this is ok").innerHTML = "wat";
fireEvent.blur(screen.getByText("wat"));
expect(screen.getByText("ok")).toBeDefined();

View File

@ -0,0 +1,10 @@
import reduceCommandBar, * as commandBarActions from '../../ducks/commandBar'
test("CommandBar", async () => {
expect(reduceCommandBar(undefined, {type: "other"})).toEqual({
visible: false
})
expect(reduceCommandBar(undefined, commandBarActions.toggleVisibility())).toEqual({
visible: true
});
});

View File

@ -187,10 +187,15 @@ describe('flows actions', () => {
test("makeSort", () => {
const a = TFlow(), b = TFlow();
a.request.scheme = "https";
a.request.method = "POST";
a.request.path = "/foo";
a.response.contentLength = 42;
a.response.status_code = 418;
Object.keys(FlowColumns).forEach((column) => {
Object.keys(FlowColumns).forEach((column, i) => {
// @ts-ignore
const sort = flowActions.makeSort({column, desc: true});
const sort = flowActions.makeSort({column, desc: i % 2 == 0});
expect(sort(a, b)).toBeDefined();
})

View File

@ -38,3 +38,29 @@ test("sendUpdate", async () => {
])
});
test("save", async () => {
enableFetchMocks();
fetchMock.mockResponseOnce("");
let store = TStore();
await store.dispatch(OptionsActions.save());
expect(fetchMock).toBeCalled();
});
test("addInterceptFilter", async () => {
enableFetchMocks();
fetchMock.mockClear();
fetchMock.mockResponses("", "");
let store = TStore();
await store.dispatch(OptionsActions.addInterceptFilter("~u foo"));
expect(fetchMock.mock.calls[0][1]?.body).toEqual('{"intercept":"~u foo"}');
store.getState().options.intercept = "~u foo";
await store.dispatch(OptionsActions.addInterceptFilter("~u foo"));
expect(fetchMock.mock.calls).toHaveLength(1);
await store.dispatch(OptionsActions.addInterceptFilter("~u bar"));
expect(fetchMock.mock.calls[1][1]?.body).toEqual('{"intercept":"~u foo | ~u bar"}');
});

View File

@ -0,0 +1,16 @@
import reduceOptionsMeta, * as OptionsMetaActions from "../../ducks/options_meta";
import * as OptionsActions from "../../ducks/options";
test("options_meta", async () => {
expect(reduceOptionsMeta(undefined, {type: "other"})).toEqual(OptionsMetaActions.defaultState);
expect(reduceOptionsMeta(undefined, {
type: OptionsActions.RECEIVE,
data: {id: {value: 'foo'}}
})).toEqual({id: {value: 'foo'}})
expect(reduceOptionsMeta(undefined, {
type: OptionsActions.UPDATE,
data: {id: {value: 1}}
})).toEqual({...OptionsMetaActions.defaultState, id: {value: 1}})
});

View File

@ -105,7 +105,7 @@ export const testState: RootState = {
viewIndex: {}, // TODO: incomplete
},
commandBar: {
visible: true,
visible: false,
}
}

View File

@ -1,5 +1,7 @@
import * as React from "react"
import {render as rtlRender} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import "@testing-library/jest-dom"
import {Provider} from 'react-redux'
// Import your own reducer
import {createAppStore} from '../ducks'
@ -9,6 +11,9 @@ import {testState} from "./ducks/tutils";
export {
waitFor, fireEvent, act, screen
} from '@testing-library/react'
export {
userEvent
}
export function render(
ui,

View File

@ -1,8 +1,25 @@
import React, { useState, useEffect, useRef } from 'react'
import React, {useEffect, useRef, useState} from 'react'
import classnames from 'classnames'
import { Key, fetchApi } from '../utils'
import {fetchApi, Key, runCommand} from '../utils'
import Filt from '../filt/command'
type CommandParameter = {
name: string
type: string
kind: string
}
type Command = {
help?: string
parameters: CommandParameter[]
return_type: string | undefined
signature_help: string
}
type AllCommands = {
[name: string]: Command
}
type CommandHelpProps = {
nextArgs: string[],
currentArg: number,
@ -12,16 +29,15 @@ type CommandHelpProps = {
}
type CommandResult = {
"id": number,
"command": string,
"result": string,
command: string,
result: string,
}
type ResultProps = {
results: CommandResult[],
}
function getAvailableCommands(commands: object, input: string = "") {
function getAvailableCommands(commands: AllCommands, input: string = ""): string[] {
if (!commands) return []
let availableCommands: string[] = []
for (const [command, args] of Object.entries(commands)) {
@ -33,21 +49,21 @@ function getAvailableCommands(commands: object, input: string = "") {
}
export function Results({results}: ResultProps) {
const resultElement= useRef<HTMLDivElement>(null!);
const resultElement = useRef<HTMLDivElement>(null!);
useEffect(() => {
if (resultElement) {
resultElement.current.addEventListener('DOMNodeInserted', (event) => {
const target = event.currentTarget as Element;
target.scroll({ top: target.scrollHeight, behavior: 'auto' });
target.scroll({top: target.scrollHeight, behavior: 'auto'});
});
}
}, [])
return (
<div className="command-result" ref={resultElement}>
{results.map(result => (
<div key={result.id}>
{results.map((result, i) => (
<div key={i}>
<div><strong>$ {result.command}</strong></div>
{result.result}
</div>
@ -56,22 +72,23 @@ export function Results({results}: ResultProps) {
)
}
export function CommandHelp({nextArgs, currentArg, help, description, availableCommands}: CommandHelpProps){
export function CommandHelp({nextArgs, currentArg, help, description, availableCommands}: CommandHelpProps) {
let argumentSuggestion: JSX.Element[] = []
for (let i: number = 0; i < nextArgs.length; i++) {
if (i==currentArg) {
argumentSuggestion.push(<mark>{nextArgs[i]}</mark>)
if (i == currentArg) {
argumentSuggestion.push(<mark key={i}>{nextArgs[i]}</mark>)
continue
}
argumentSuggestion.push(<span>{nextArgs[i]} </span>)
argumentSuggestion.push(<span key={i}>{nextArgs[i]} </span>)
}
return (<div className="argument-suggestion popover top">
<div className="arrow"/>
<div className="popover-content">
{ argumentSuggestion.length > 0 && <div><strong>Argument suggestion:</strong> {argumentSuggestion}</div> }
{ help?.includes("->") && <div><strong>Signature help: </strong>{help}</div>}
{ description && <div># {description}</div>}
<div><strong>Available Commands: </strong><p className="available-commands">{JSON.stringify(availableCommands)}</p></div>
{argumentSuggestion.length > 0 && <div><strong>Argument suggestion:</strong> {argumentSuggestion}</div>}
{help?.includes("->") && <div><strong>Signature help: </strong>{help}</div>}
{description && <div># {description}</div>}
<div><strong>Available Commands: </strong><p
className="available-commands">{JSON.stringify(availableCommands)}</p></div>
</div>
</div>)
}
@ -83,7 +100,7 @@ export default function CommandBar() {
const [completionCandidate, setCompletionCandidate] = useState<string[]>([])
const [availableCommands, setAvailableCommands] = useState<string[]>([])
const [allCommands, setAllCommands] = useState<object>({})
const [allCommands, setAllCommands] = useState<AllCommands>({})
const [nextArgs, setNextArgs] = useState<string[]>([])
const [currentArg, setCurrentArg] = useState<number>(0)
const [signatureHelp, setSignatureHelp] = useState<string>("")
@ -91,15 +108,21 @@ export default function CommandBar() {
const [results, setResults] = useState<CommandResult[]>([])
const [history, setHistory] = useState<string[]>([])
const [currentPos, setCurrentPos] = useState<number>(0)
const [currentPos, setCurrentPos] = useState<number | undefined>(undefined);
useEffect(() => {
fetchApi('/commands', { method: 'GET' })
.then(response => response.json())
.then(data => {
setAllCommands(data["commands"])
setCompletionCandidate(getAvailableCommands(data["commands"]))
setAvailableCommands(Object.keys(data))
fetchApi('/commands', {method: 'GET'})
.then(response => response.json())
.then((data: AllCommands) => {
setAllCommands(data)
setCompletionCandidate(getAvailableCommands(data))
setAvailableCommands(Object.keys(data))
}).catch(e => console.error(e))
}, [])
useEffect(() => {
runCommand("commands.history.get").then((ret) => {
setHistory(ret.value);
}).catch(e => console.error(e))
}, [])
@ -108,20 +131,20 @@ export default function CommandBar() {
const originalParts: string[] = Filt.parse(originalInput)
setSignatureHelp(allCommands[parts[0]]?.signature_help)
setDescription(allCommands[parts[0]]?.description)
setDescription(allCommands[parts[0]]?.help || "")
setCompletionCandidate(getAvailableCommands(allCommands, originalParts[0]))
setAvailableCommands(getAvailableCommands(allCommands, parts[0]))
const nextArgs: string[] = allCommands[parts[0]]?.args
const nextArgs: string[] = allCommands[parts[0]]?.parameters.map(p => p.name)
if (nextArgs) {
setNextArgs([parts[0], ...nextArgs])
setCurrentArg(parts.length-1)
setCurrentArg(parts.length - 1)
}
}
const onChange = (e) => {
const onChange = (e) => {
setInput(e.target.value)
setOriginalInput(e.target.value)
setCurrentCompletion(0)
@ -129,25 +152,27 @@ export default function CommandBar() {
const onKeyDown = (e) => {
if (e.keyCode === Key.ENTER) {
const body = {"command": input}
const [cmd, ...args] = Filt.parse(input);
fetchApi(`/commands`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
setHistory(data.history)
setCurrentPos(currentPos + 1)
setHistory([...history, input]);
runCommand("commands.history.add", input).catch(() => 0);
fetchApi.post(`/commands/${cmd}`, {arguments: args})
.then(response => response.json())
.then(data => {
setCurrentPos(undefined)
setNextArgs([])
setResults([...results, {
command: input,
result: JSON.stringify(data.value || data.error)
}])
}).catch(e => {
setCurrentPos(undefined)
setNextArgs([])
setResults([...results, {
"id": results.length,
"command": input,
"result": JSON.stringify(data.result)
}])
command: input,
result: e.toString()
}]);
})
setSignatureHelp("")
@ -160,17 +185,28 @@ export default function CommandBar() {
setCompletionCandidate(availableCommands)
}
if (e.keyCode === Key.UP) {
if (currentPos > 0) {
setInput(history[currentPos - 1])
setOriginalInput(history[currentPos -1])
setCurrentPos(currentPos - 1)
let nextPos;
if (currentPos === undefined) {
nextPos = history.length - 1;
} else {
nextPos = Math.max(0, currentPos - 1);
}
setInput(history[nextPos])
setOriginalInput(history[nextPos])
setCurrentPos(nextPos)
}
if (e.keyCode === Key.DOWN) {
setInput(history[currentPos])
setOriginalInput(history[currentPos])
if (currentPos < history.length -1) {
setCurrentPos(currentPos + 1)
if (currentPos === undefined) {
return
} else if (currentPos == history.length - 1) {
setInput("");
setOriginalInput("");
setCurrentPos(undefined);
} else {
const nextPos = currentPos + 1;
setInput(history[nextPos])
setOriginalInput(history[nextPos])
setCurrentPos(nextPos)
}
}
if (e.keyCode === Key.TAB) {
@ -195,8 +231,9 @@ export default function CommandBar() {
<div className="command-title">
Command Result
</div>
<Results results={results} />
<CommandHelp nextArgs={nextArgs} currentArg={currentArg} help={signatureHelp} description={description} availableCommands={availableCommands} />
<Results results={results}/>
<CommandHelp nextArgs={nextArgs} currentArg={currentArg} help={signatureHelp} description={description}
availableCommands={availableCommands}/>
<div className={classnames('command-input input-group')}>
<span className="input-group-addon">
<i className={'fa fa-fw fa-terminal'}/>
@ -205,7 +242,7 @@ export default function CommandBar() {
type="text"
placeholder="Enter command"
className="form-control"
value={input}
value={input || ""}
onChange={onChange}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}

View File

@ -6,7 +6,7 @@ interface CommandBarState {
visible: boolean
}
const defaultState: CommandBarState = {
export const defaultState: CommandBarState = {
visible: false,
};

View File

@ -112,16 +112,11 @@ export function makeFilter(filter?: string): FlowFilterFn | undefined {
return Filt.parse(filter)
}
export function makeSort({column, desc}: { column: keyof typeof FlowColumns, desc: boolean }): FlowSortFn;
export function makeSort({column, desc}: { column?: keyof typeof FlowColumns, desc: boolean }): FlowSortFn | undefined;
export function makeSort({column, desc}: { column?: keyof typeof FlowColumns, desc: boolean }): FlowSortFn | undefined {
export function makeSort({column, desc}: { column?: keyof typeof FlowColumns, desc: boolean }): FlowSortFn {
if (!column) {
return
return (a,b) => 0;
}
const sortKeyFun = FlowColumns[column].sortKey
if (!sortKeyFun) {
return
}
return (a, b) => {
const ka = sortKeyFun(a)
const kb = sortKeyFun(b)

View File

@ -49,7 +49,7 @@ export async function pureSendUpdate(option: Option, value, dispatch) {
}
}
let sendUpdate = _.throttle(pureSendUpdate, 500, {leading: true, trailing: true})
let sendUpdate = pureSendUpdate; // _.throttle(pureSendUpdate, 500, {leading: true, trailing: true})
export function update(name: Option, value: any): AppThunk {
return dispatch => {

View File

@ -14,7 +14,7 @@ type OptionsMetaState = Partial<{
[name in keyof OptionsState]: OptionMeta<OptionsState[name]>
}>
const defaultState: OptionsMetaState = {
export const defaultState: OptionsMetaState = {
}
const reducer: Reducer<OptionsMetaState> = (state = defaultState, action) => {

View File

@ -116,6 +116,19 @@ fetchApi.put = (url: string, json: any, options: RequestInit = {}) => fetchApi(
}
)
fetchApi.post = (url: string, json: any, options: RequestInit = {}) => fetchApi(
url,
{
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(json),
...options
}
)
export async function runCommand(command: string, ...args): Promise<any> {
let response = await fetchApi(`/commands/${command}`, {
method: 'POST', headers: {