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

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", "coverageDirectory": "./coverage",
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"<rootDir>/src/js/filt/filt.js", "<rootDir>/src/js/contrib/",
"<rootDir>/src/js/filt/command.js" "<rootDir>/src/js/filt/",
"<rootDir>/src/js/components/editors/"
], ],
"collectCoverageFrom": [ "collectCoverageFrom": [
"src/js/**/*.{js,jsx,ts,tsx}" "src/js/**/*.{js,jsx,ts,tsx}"

152
web/package-lock.json generated
View File

@ -662,12 +662,12 @@
} }
}, },
"@babel/runtime-corejs3": { "@babel/runtime-corejs3": {
"version": "7.14.6", "version": "7.15.3",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz",
"integrity": "sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==", "integrity": "sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==",
"dev": true, "dev": true,
"requires": { "requires": {
"core-js-pure": "^3.14.0", "core-js-pure": "^3.16.0",
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
}, },
@ -1025,9 +1025,9 @@
} }
}, },
"@testing-library/dom": { "@testing-library/dom": {
"version": "7.31.2", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.1.0.tgz",
"integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==", "integrity": "sha512-kmW9alndr19qd6DABzQ978zKQ+J65gU2Rzkl8hriIetPnwpesRaK4//jEQyYh8fEALmGhomD/LBQqt+o+DL95Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
@ -1037,7 +1037,71 @@
"chalk": "^4.1.0", "chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.6", "dom-accessibility-api": "^0.5.6",
"lz-string": "^1.4.4", "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": { "dependencies": {
"@jest/types": { "@jest/types": {
@ -1053,10 +1117,26 @@
"chalk": "^4.0.0" "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": { "@types/yargs": {
"version": "15.0.13", "version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
"integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@ -1076,14 +1156,13 @@
} }
} }
}, },
"@testing-library/react": { "@testing-library/user-event": {
"version": "11.2.7", "version": "13.2.1",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.2.1.tgz",
"integrity": "sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA==", "integrity": "sha512-cczlgVl+krjOb3j1625usarNEibI0IFRJrSWX9UsJ1HKYFgCQv9Nb7QAipUDXl3Xdz8NDTsiS78eAkPSxlzTlw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5"
"@testing-library/dom": "^7.28.1"
} }
}, },
"@tootallnate/once": { "@tootallnate/once": {
@ -1093,9 +1172,9 @@
"dev": true "dev": true
}, },
"@types/aria-query": { "@types/aria-query": {
"version": "4.2.1", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==", "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
"dev": true "dev": true
}, },
"@types/babel__core": { "@types/babel__core": {
@ -1336,6 +1415,15 @@
"integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
"dev": true "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": { "@types/vinyl": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.4.tgz",
@ -2652,9 +2740,9 @@
} }
}, },
"core-js-pure": { "core-js-pure": {
"version": "3.15.0", "version": "3.16.2",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.0.tgz", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.2.tgz",
"integrity": "sha512-RO+LFAso8DB6OeBX9BAcEGvyth36QtxYon1OyVsITNVtSKr/Hos0BXZwnsOJ7o+O6KHtK+O+cJIEj9NGg6VwFA==", "integrity": "sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==",
"dev": true "dev": true
}, },
"core-util-is": { "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": { "cssom": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -2954,9 +3048,9 @@
"dev": true "dev": true
}, },
"dom-accessibility-api": { "dom-accessibility-api": {
"version": "0.5.6", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz",
"integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==", "integrity": "sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==",
"dev": true "dev": true
}, },
"domexception": { "domexception": {
@ -6666,6 +6760,12 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true "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": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",

View File

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

View File

@ -1,26 +1,67 @@
import * as React from "react" 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 CommandBar from "../../components/CommandBar";
import fetchMock, {enableFetchMocks} from "jest-fetch-mock"; import fetchMock, {enableFetchMocks} from "jest-fetch-mock";
enableFetchMocks(); enableFetchMocks();
test("CommandBar", async () => { test("CommandBar", async () => {
fetchMock.mockResponseOnce(JSON.stringify({ fetchMock.mockOnceIf("./commands", JSON.stringify({
"flow.decode": {"help": "Decode flows.", "flow.decode": {
"parameters": [{"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"}, { "help": "Decode flows.",
"name": "part", "parameters": [
"type": "str", {"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"},
"kind": "POSITIONAL_OR_KEYWORD" {"name": "part", "type": "str", "kind": "POSITIONAL_OR_KEYWORD"}
}], ],
"return_type": null, "return_type": null,
"signature_help": "flow.decode flows part" "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/>); const {asFragment} = render(<CommandBar/>);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
await waitFor(() => screen.getByText('["flow.decode"]')) await waitFor(() => screen.getByText('["flow.decode","flow.encode"]'))
expect(asFragment()).toMatchSnapshot(); 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> <label>
<input <input
checked={true} checked={false}
onChange={[Function]} onChange={[Function]}
type="checkbox" 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 <p
class="available-commands" class="available-commands"
> >
["flow.decode"] ["flow.decode","flow.encode"]
</p> </p>
</div> </div>
</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 * as React from "react"
import ValidateEditor from '../../../components/editors/ValidateEditor' 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 () => { test("ValidateEditor", async () => {
const onEditDone = jest.fn(); const onEditDone = jest.fn();
@ -9,8 +9,7 @@ test("ValidateEditor", async () => {
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
fireEvent.mouseDown(screen.getByText("ok")); userEvent.click(screen.getByText("ok"));
fireEvent.mouseUp(screen.getByText("ok"));
screen.getByText("ok").innerHTML = "this is ok"; screen.getByText("ok").innerHTML = "this is ok";
@ -19,8 +18,7 @@ test("ValidateEditor", async () => {
await waitFor(() => expect(onEditDone).toBeCalledWith("this is ok")); await waitFor(() => expect(onEditDone).toBeCalledWith("this is ok"));
onEditDone.mockClear(); onEditDone.mockClear();
fireEvent.mouseDown(screen.getByText("this is ok")); userEvent.click(screen.getByText("this is ok"));
fireEvent.mouseUp(screen.getByText("this is ok"));
screen.getByText("this is ok").innerHTML = "wat"; screen.getByText("this is ok").innerHTML = "wat";
fireEvent.blur(screen.getByText("wat")); fireEvent.blur(screen.getByText("wat"));
expect(screen.getByText("ok")).toBeDefined(); 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", () => { test("makeSort", () => {
const a = TFlow(), b = TFlow(); 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 // @ts-ignore
const sort = flowActions.makeSort({column, desc: true}); const sort = flowActions.makeSort({column, desc: i % 2 == 0});
expect(sort(a, b)).toBeDefined(); 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 viewIndex: {}, // TODO: incomplete
}, },
commandBar: { commandBar: {
visible: true, visible: false,
} }
} }

View File

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

View File

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

View File

@ -112,16 +112,11 @@ export function makeFilter(filter?: string): FlowFilterFn | undefined {
return Filt.parse(filter) 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 {
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 {
if (!column) { if (!column) {
return return (a,b) => 0;
} }
const sortKeyFun = FlowColumns[column].sortKey const sortKeyFun = FlowColumns[column].sortKey
if (!sortKeyFun) {
return
}
return (a, b) => { return (a, b) => {
const ka = sortKeyFun(a) const ka = sortKeyFun(a)
const kb = sortKeyFun(b) 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 { export function update(name: Option, value: any): AppThunk {
return dispatch => { return dispatch => {

View File

@ -14,7 +14,7 @@ type OptionsMetaState = Partial<{
[name in keyof OptionsState]: OptionMeta<OptionsState[name]> [name in keyof OptionsState]: OptionMeta<OptionsState[name]>
}> }>
const defaultState: OptionsMetaState = { export const defaultState: OptionsMetaState = {
} }
const reducer: Reducer<OptionsMetaState> = (state = defaultState, action) => { 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> { export async function runCommand(command: string, ...args): Promise<any> {
let response = await fetchApi(`/commands/${command}`, { let response = await fetchApi(`/commands/${command}`, {
method: 'POST', headers: { method: 'POST', headers: {