diff options
24 files changed, 637 insertions, 36 deletions
diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index 3c3e1c65..54526d5b 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -174,7 +174,7 @@ class Dumper: # This aligns the HTTP response code with the HTTP request method: # 127.0.0.1:59519: GET http://example.com/ # << 304 Not Modified 0b - arrows = " " * (len(repr(flow.client_conn.address)) - 2) + arrows + arrows = " " * (len(human.format_address(flow.client_conn.address)) - 2) + arrows line = "{replay}{arrows} {code} {reason} {size}".format( replay=replay, @@ -224,7 +224,7 @@ class Dumper: def websocket_error(self, f): self.echo( "Error in WebSocket connection to {}: {}".format( - repr(f.server_conn.address), f.error + human.format_address(f.server_conn.address), f.error ), fg="red" ) @@ -247,7 +247,7 @@ class Dumper: def tcp_error(self, f): self.echo( "Error in TCP connection to {}: {}".format( - repr(f.server_conn.address), f.error + human.format_address(f.server_conn.address), f.error ), fg="red" ) @@ -257,8 +257,8 @@ class Dumper: message = f.messages[-1] direction = "->" if message.from_client else "<-" self.echo("{client} {direction} tcp {direction} {server}".format( - client=repr(f.client_conn.address), - server=repr(f.server_conn.address), + client=human.format_address(f.client_conn.address), + server=human.format_address(f.server_conn.address), direction=direction, )) if ctx.options.flow_detail >= 3: diff --git a/mitmproxy/addons/onboardingapp/templates/index.html b/mitmproxy/addons/onboardingapp/templates/index.html index c8d0f07a..fc6213ea 100644 --- a/mitmproxy/addons/onboardingapp/templates/index.html +++ b/mitmproxy/addons/onboardingapp/templates/index.html @@ -55,7 +55,7 @@ function changeTo(device) { </script> <center> -<h2> Click to install the mitmproxy certificate: </h2> +<h2> Click to install your mitmproxy certificate: </h2> </center> <div id="certbank" class="row"> <div class="col-md-3"> diff --git a/mitmproxy/contentviews/image/image_parser.py b/mitmproxy/contentviews/image/image_parser.py index 7c74669a..fcc50cb5 100644 --- a/mitmproxy/contentviews/image/image_parser.py +++ b/mitmproxy/contentviews/image/image_parser.py @@ -6,6 +6,7 @@ from kaitaistruct import KaitaiStream from mitmproxy.contrib.kaitaistruct import png from mitmproxy.contrib.kaitaistruct import gif from mitmproxy.contrib.kaitaistruct import jpeg +from mitmproxy.contrib.kaitaistruct import ico Metadata = typing.List[typing.Tuple[str, str]] @@ -78,3 +79,25 @@ def parse_jpeg(data: bytes) -> Metadata: if field.data is not None: parts.append((field.tag._name_, field.data.decode('UTF-8').strip('\x00'))) return parts + + +def parse_ico(data: bytes) -> Metadata: + img = ico.Ico(KaitaiStream(io.BytesIO(data))) + parts = [ + ('Format', 'ICO'), + ('Number of images', str(img.num_images)), + ] + + for i, image in enumerate(img.images): + parts.append( + ( + 'Image {}'.format(i + 1), "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(256 if not image.width else image.width, + 256 if not image.height else image.height, + '', image.bpp, + '', image.is_png) + ) + ) + + return parts diff --git a/mitmproxy/contentviews/image/view.py b/mitmproxy/contentviews/image/view.py index 95ee1e43..6f75473b 100644 --- a/mitmproxy/contentviews/image/view.py +++ b/mitmproxy/contentviews/image/view.py @@ -5,6 +5,14 @@ from mitmproxy.types import multidict from . import image_parser +def test_ico(h, f): + if h.startswith(b"\x00\x00\x01\x00"): + return "ico" + + +imghdr.tests.append(test_ico) + + class ViewImage(base.View): name = "Image" prompt = ("image", "i") @@ -27,6 +35,8 @@ class ViewImage(base.View): image_metadata = image_parser.parse_gif(data) elif image_type == 'jpeg': image_metadata = image_parser.parse_jpeg(data) + elif image_type == 'ico': + image_metadata = image_parser.parse_ico(data) else: image_metadata = [ ("Image Format", image_type or "unknown") diff --git a/mitmproxy/contrib/kaitaistruct/ico.py b/mitmproxy/contrib/kaitaistruct/ico.py new file mode 100644 index 00000000..94b1b8d9 --- /dev/null +++ b/mitmproxy/contrib/kaitaistruct/ico.py @@ -0,0 +1,90 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +from pkg_resources import parse_version +from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +import struct + + +if parse_version(ks_version) < parse_version('0.7'): + raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) + +class Ico(KaitaiStruct): + """Microsoft Windows uses specific file format to store applications + icons - ICO. This is a container that contains one or more image + files (effectively, DIB parts of BMP files or full PNG files are + contained inside). + + .. seealso:: + Source - https://msdn.microsoft.com/en-us/library/ms997538.aspx + """ + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.magic = self._io.ensure_fixed_contents(struct.pack('4b', 0, 0, 1, 0)) + self.num_images = self._io.read_u2le() + self.images = [None] * (self.num_images) + for i in range(self.num_images): + self.images[i] = self._root.IconDirEntry(self._io, self, self._root) + + + class IconDirEntry(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.width = self._io.read_u1() + self.height = self._io.read_u1() + self.num_colors = self._io.read_u1() + self.reserved = self._io.ensure_fixed_contents(struct.pack('1b', 0)) + self.num_planes = self._io.read_u2le() + self.bpp = self._io.read_u2le() + self.len_img = self._io.read_u4le() + self.ofs_img = self._io.read_u4le() + + @property + def img(self): + """Raw image data. Use `is_png` to determine whether this is an + embedded PNG file (true) or a DIB bitmap (false) and call a + relevant parser, if needed to parse image data further. + """ + if hasattr(self, '_m_img'): + return self._m_img if hasattr(self, '_m_img') else None + + _pos = self._io.pos() + self._io.seek(self.ofs_img) + self._m_img = self._io.read_bytes(self.len_img) + self._io.seek(_pos) + return self._m_img if hasattr(self, '_m_img') else None + + @property + def png_header(self): + """Pre-reads first 8 bytes of the image to determine if it's an + embedded PNG file. + """ + if hasattr(self, '_m_png_header'): + return self._m_png_header if hasattr(self, '_m_png_header') else None + + _pos = self._io.pos() + self._io.seek(self.ofs_img) + self._m_png_header = self._io.read_bytes(8) + self._io.seek(_pos) + return self._m_png_header if hasattr(self, '_m_png_header') else None + + @property + def is_png(self): + """True if this image is in PNG format.""" + if hasattr(self, '_m_is_png'): + return self._m_is_png if hasattr(self, '_m_is_png') else None + + self._m_is_png = self.png_header == struct.pack('8b', -119, 80, 78, 71, 13, 10, 26, 10) + return self._m_is_png if hasattr(self, '_m_is_png') else None + + + diff --git a/mitmproxy/contrib/kaitaistruct/make.sh b/mitmproxy/contrib/kaitaistruct/make.sh index 9ef68886..789829cf 100755 --- a/mitmproxy/contrib/kaitaistruct/make.sh +++ b/mitmproxy/contrib/kaitaistruct/make.sh @@ -6,6 +6,6 @@ wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/gif.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/jpeg.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/png.ksy -wget -N https://raw.githubusercontent.com/mitmproxy/mitmproxy/master/mitmproxy/contrib/tls_client_hello.py +wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/ico.ksy kaitai-struct-compiler --target python --opaque-types=true *.ksy diff --git a/mitmproxy/contrib/tls_client_hello.ksy b/mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy index 5b6eb0fb..921c11b5 100644 --- a/mitmproxy/contrib/tls_client_hello.ksy +++ b/mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy @@ -59,14 +59,9 @@ types: type: u2 - id: cipher_suites - type: cipher_suite + type: u2 repeat: expr repeat-expr: len/2 - - cipher_suite: - seq: - - id: cipher_suite - type: u2 compression_methods: seq: diff --git a/mitmproxy/contrib/kaitaistruct/tls_client_hello.py b/mitmproxy/contrib/kaitaistruct/tls_client_hello.py index 6aff9b14..10e5367f 100644 --- a/mitmproxy/contrib/kaitaistruct/tls_client_hello.py +++ b/mitmproxy/contrib/kaitaistruct/tls_client_hello.py @@ -73,7 +73,7 @@ class TlsClientHello(KaitaiStruct): self.len = self._io.read_u2be() self.cipher_suites = [None] * (self.len // 2) for i in range(self.len // 2): - self.cipher_suites[i] = self._root.CipherSuite(self._io, self, self._root) + self.cipher_suites[i] = self._io.read_u2be() class CompressionMethods(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): @@ -111,13 +111,6 @@ class TlsClientHello(KaitaiStruct): self.major = self._io.read_u1() self.minor = self._io.read_u1() - class CipherSuite(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self.cipher_suite = self._io.read_u2be() - class Protocol(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 1680d346..68c2f975 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -401,21 +401,36 @@ def dump_defaults(opts): if o.choices: txt += " Valid values are %s." % ", ".join(repr(c) for c in o.choices) else: - if o.typespec in (str, int, bool): - t = o.typespec.__name__ - elif o.typespec == typing.Optional[str]: - t = "optional str" - elif o.typespec == typing.Sequence[str]: - t = "sequence of str" - else: # pragma: no cover - raise NotImplementedError + t = typecheck.typespec_to_str(o.typespec) txt += " Type %s." % t txt = "\n".join(textwrap.wrap(txt)) - s.yaml_set_comment_before_after_key(k, before = "\n" + txt) + s.yaml_set_comment_before_after_key(k, before="\n" + txt) return ruamel.yaml.round_trip_dump(s) +def dump_dicts(opts): + """ + Dumps the options into a list of dict object. + + Return: A list like: [ { name: "anticache", type: "bool", default: false, value: true, help: "help text"} ] + """ + options_list = [] + for k in sorted(opts.keys()): + o = opts._options[k] + t = typecheck.typespec_to_str(o.typespec) + option = { + 'name': k, + 'type': t, + 'default': o.default, + 'value': o.current(), + 'help': o.help, + 'choices': o.choices + } + options_list.append(option) + return options_list + + def parse(text): if not text: return {} diff --git a/mitmproxy/proxy/protocol/tls.py b/mitmproxy/proxy/protocol/tls.py index d42c7fdd..b7bc6b1c 100644 --- a/mitmproxy/proxy/protocol/tls.py +++ b/mitmproxy/proxy/protocol/tls.py @@ -539,8 +539,8 @@ class TlsLayer(base.Layer): if not ciphers_server and self._client_tls: ciphers_server = [] for id in self._client_hello.cipher_suites: - if id.cipher_suite in CIPHER_ID_NAME_MAP.keys(): - ciphers_server.append(CIPHER_ID_NAME_MAP[id.cipher_suite]) + if id in CIPHER_ID_NAME_MAP.keys(): + ciphers_server.append(CIPHER_ID_NAME_MAP[id]) ciphers_server = ':'.join(ciphers_server) self.server_conn.establish_ssl( diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index c55c0cb5..8b4a39b6 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -17,6 +17,7 @@ from mitmproxy import http from mitmproxy import io from mitmproxy import log from mitmproxy import version +from mitmproxy import optmanager import mitmproxy.tools.web.master # noqa @@ -438,6 +439,18 @@ class Settings(RequestHandler): self.master.options.update(**update) +class Options(RequestHandler): + def get(self): + self.write(optmanager.dump_dicts(self.master.options)) + + def put(self): + update = self.json + try: + self.master.options.update(**update) + except (KeyError, TypeError) as err: + raise APIError(400, "{}".format(err)) + + class Application(tornado.web.Application): def __init__(self, master, debug): self.master = master @@ -462,6 +475,7 @@ class Application(tornado.web.Application): FlowContentView), (r"/settings", Settings), (r"/clear", ClearAll), + (r"/options", Options) ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), diff --git a/mitmproxy/utils/typecheck.py b/mitmproxy/utils/typecheck.py index a5f27fee..87a0e804 100644 --- a/mitmproxy/utils/typecheck.py +++ b/mitmproxy/utils/typecheck.py @@ -98,3 +98,15 @@ def check_option_type(name: str, value: typing.Any, typeinfo: typing.Any) -> Non return elif not isinstance(value, typeinfo): raise e + + +def typespec_to_str(typespec: typing.Any) -> str: + if typespec in (str, int, bool): + t = typespec.__name__ + elif typespec == typing.Optional[str]: + t = 'optional str' + elif typespec == typing.Sequence[str]: + t = 'sequence of str' + else: + raise NotImplementedError + return t diff --git a/test/mitmproxy/contentviews/image/test_image_parser.py b/test/mitmproxy/contentviews/image/test_image_parser.py index 3cb44ca6..fdc72165 100644 --- a/test/mitmproxy/contentviews/image/test_image_parser.py +++ b/test/mitmproxy/contentviews/image/test_image_parser.py @@ -167,3 +167,26 @@ def test_parse_gif(filename, metadata): def test_parse_jpeg(filename, metadata): with open(tutils.test_data.path(filename), 'rb') as f: assert metadata == image_parser.parse_jpeg(f.read()) + + +@pytest.mark.parametrize("filename, metadata", { + "mitmproxy/data/image.ico": [ + ('Format', 'ICO'), + ('Number of images', '3'), + ('Image 1', "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(48, 48, '', 24, '', False) + ), + ('Image 2', "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(32, 32, '', 24, '', False) + ), + ('Image 3', "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(16, 16, '', 24, '', False) + ) + ] +}.items()) +def test_ico(filename, metadata): + with open(tutils.test_data.path(filename), 'rb') as f: + assert metadata == image_parser.parse_ico(f.read()) diff --git a/test/mitmproxy/contentviews/image/test_view.py b/test/mitmproxy/contentviews/image/test_view.py index 34f655a1..6da5b1d0 100644 --- a/test/mitmproxy/contentviews/image/test_view.py +++ b/test/mitmproxy/contentviews/image/test_view.py @@ -9,8 +9,7 @@ def test_view_image(): "mitmproxy/data/image.png", "mitmproxy/data/image.gif", "mitmproxy/data/all.jpeg", - # https://bugs.python.org/issue21574 - # "mitmproxy/data/image.ico", + "mitmproxy/data/image.ico", ]: with open(tutils.test_data.path(img), "rb") as f: viewname, lines = v(f.read()) diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index cadc5d76..0c400683 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -338,6 +338,11 @@ def test_dump_defaults(): assert optmanager.dump_defaults(o) +def test_dump_dicts(): + o = options.Options() + assert optmanager.dump_dicts(o) + + class TTypes(optmanager.OptManager): def __init__(self): super().__init__() diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index 5427b995..e6d563e7 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -253,6 +253,16 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): assert self.put_json("/settings", {"anticache": True}).code == 200 assert self.put_json("/settings", {"wtf": True}).code == 400 + def test_options(self): + j = json(self.fetch("/options")) + assert type(j) == list + assert type(j[0]) == dict + + def test_option_update(self): + assert self.put_json("/options", {"anticache": True}).code == 200 + assert self.put_json("/options", {"wtf": True}).code == 400 + assert self.put_json("/options", {"anticache": "foo"}).code == 400 + def test_err(self): with mock.patch("mitmproxy.tools.web.app.IndexHandler.get") as f: f.side_effect = RuntimeError diff --git a/test/mitmproxy/utils/test_typecheck.py b/test/mitmproxy/utils/test_typecheck.py index fe33070e..66b1884e 100644 --- a/test/mitmproxy/utils/test_typecheck.py +++ b/test/mitmproxy/utils/test_typecheck.py @@ -111,3 +111,11 @@ def test_check_command_type(): m.__str__ = lambda self: "typing.Union" m.__union_params__ = (int,) assert not typecheck.check_command_type([22], m) + + +def test_typesec_to_str(): + assert(typecheck.typespec_to_str(str)) == "str" + assert(typecheck.typespec_to_str(typing.Sequence[str])) == "sequence of str" + assert(typecheck.typespec_to_str(typing.Optional[str])) == "optional str" + with pytest.raises(NotImplementedError): + typecheck.typespec_to_str(dict) diff --git a/web/src/js/__tests__/components/ContentViewSpec.js b/web/src/js/__tests__/components/ContentViewSpec.js new file mode 100644 index 00000000..a654870e --- /dev/null +++ b/web/src/js/__tests__/components/ContentViewSpec.js @@ -0,0 +1,62 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import ContentView from '../../components/ContentView' +import { TStore, TFlow } from '../ducks/tutils' +import { Provider } from 'react-redux' +import mockXMLHttpRequest from 'mock-xmlhttprequest' + +global.XMLHttpRequest = mockXMLHttpRequest + +describe('ContentView Component', () => { + let store = TStore() + + it('should render correctly', () => { + store.getState().ui.flow.contentView = 'Edit' + let tflow = TFlow(), + provider = renderer.create( + <Provider store={store}> + <ContentView flow={tflow} message={tflow.request}/> + </Provider>), + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render correctly with empty content', () => { + let tflow = TFlow() + tflow.response.contentLength = 0 + let provider = renderer.create( + <Provider store={store}> + <ContentView flow={tflow} message={tflow.response} readonly={true}/> + </Provider>), + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render correctly with missing content', () => { + let tflow = TFlow() + tflow.response.contentLength = null + let provider = renderer.create( + <Provider store={store}> + <ContentView flow={tflow} message={tflow.response} readonly={true}/> + </Provider>), + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should render correctly with content too large', () => { + let tflow = TFlow() + tflow.response.contentLength = 1024 * 1024 * 100 + let provider = renderer.create( + <Provider store={store}> + <ContentView + flow={tflow} + message={tflow.response} + readonly={true} + uploadContent={jest.fn()} + onOpenFile={jest.fn()} + /> + </Provider>), + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/web/src/js/__tests__/components/EventLogSpec.js b/web/src/js/__tests__/components/EventLogSpec.js new file mode 100644 index 00000000..8510de55 --- /dev/null +++ b/web/src/js/__tests__/components/EventLogSpec.js @@ -0,0 +1,57 @@ +jest.mock('../../components/EventLog/EventList') + +import React from 'react' +import renderer from 'react-test-renderer' +import TestUtils from 'react-dom/test-utils' +import EventLog, { PureEventLog } from '../../components/EventLog' +import { Provider } from 'react-redux' +import { TStore } from '../ducks/tutils' + +window.addEventListener = jest.fn() +window.removeEventListener = jest.fn() + +describe('EventLog Component', () => { + let store = TStore(), + provider = renderer.create( + <Provider store={store}> + <EventLog/> + </Provider>), + tree = provider.toJSON() + + it('should connect to state and render correctly', () => { + expect(tree).toMatchSnapshot() + }) + + it('should handle toggleFilter', () => { + let debugToggleButton = tree.children[0].children[1].children[0] + debugToggleButton.props.onClick() + }) + + provider = TestUtils.renderIntoDocument( + <Provider store={store}><EventLog/></Provider>) + let eventLog = TestUtils.findRenderedComponentWithType(provider, PureEventLog), + mockEvent = { preventDefault: jest.fn() } + + it('should handle DragStart', () => { + eventLog.onDragStart(mockEvent) + expect(mockEvent.preventDefault).toBeCalled() + expect(window.addEventListener).toBeCalledWith('mousemove', eventLog.onDragMove) + expect(window.addEventListener).toBeCalledWith('mouseup', eventLog.onDragStop) + expect(window.addEventListener).toBeCalledWith('dragend', eventLog.onDragStop) + mockEvent.preventDefault.mockClear() + }) + + it('should handle DragMove', () => { + eventLog.onDragMove(mockEvent) + expect(mockEvent.preventDefault).toBeCalled() + mockEvent.preventDefault.mockClear() + }) + + console.error = jest.fn() // silent the error. + it('should handle DragStop', () => { + eventLog.onDragStop(mockEvent) + expect(mockEvent.preventDefault).toBeCalled() + expect(window.removeEventListener).toBeCalledWith('mousemove', eventLog.onDragMove) + }) + +}) diff --git a/web/src/js/__tests__/components/FlowTableSpec.js b/web/src/js/__tests__/components/FlowTableSpec.js new file mode 100644 index 00000000..4d8de12c --- /dev/null +++ b/web/src/js/__tests__/components/FlowTableSpec.js @@ -0,0 +1,50 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import FlowTable from '../../components/FlowTable' +import TestUtils from 'react-dom/test-utils' +import { TFlow, TStore } from '../ducks/tutils' +import { Provider } from 'react-redux' + +window.addEventListener = jest.fn() + +describe('FlowTable Component', () => { + let selectFn = jest.fn(), + tflow = TFlow(), + store = TStore() + + it('should render correctly', () => { + let provider = renderer.create( + <Provider store={store}> + <FlowTable onSelect={selectFn} flows={[tflow]}/> + </Provider>), + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + }) + + let provider = TestUtils.renderIntoDocument( + <Provider store={store} > + <FlowTable onSelect={selectFn} flows={[tflow]}/> + </Provider>), + flowTable = TestUtils.findRenderedComponentWithType(provider, FlowTable) + + it('should handle componentWillUnmount', () => { + flowTable.componentWillUnmount() + expect(window.addEventListener).toBeCalledWith('resize', flowTable.onViewportUpdate) + }) + + it('should handle componentDidUpdate', () => { + // flowTable.shouldScrollIntoView == false + expect(flowTable.componentDidUpdate()).toEqual(undefined) + // rowTop - headHeight < viewportTop + flowTable.shouldScrollIntoView = true + flowTable.componentDidUpdate() + // rowBottom > viewportTop + viewportHeight + flowTable.shouldScrollIntoView = true + flowTable.componentDidUpdate() + }) + + it('should handle componentWillReceiveProps', () => { + flowTable.componentWillReceiveProps({selected: tflow}) + expect(flowTable.shouldScrollIntoView).toBeTruthy() + }) +}) diff --git a/web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap b/web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap new file mode 100644 index 00000000..60b816e2 --- /dev/null +++ b/web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContentView Component should render correctly 1`] = ` +<div + className="contentview" +> + <div + className="text-center" + > + <i + className="fa fa-spinner fa-spin" + /> + </div> +</div> +`; + +exports[`ContentView Component should render correctly with content too large 1`] = ` +<div> + <div + className="alert alert-warning" + > + <button + className="btn btn-xs btn-warning pull-right" + onClick={[Function]} + > + Display anyway + </button> + 100mb + content size. + </div> + <div + className="view-options text-center" + > + <a + className="btn btn-default btn-xs" + href="#" + onClick={[Function]} + title="Upload a file to replace the content." + > + <i + className="fa fa-fw fa-upload" + /> + <input + className="hidden" + onChange={[Function]} + type="file" + /> + </a> + + <a + className="btn btn-default btn-xs" + href="/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content" + title="Download the content of the flow." + > + <i + className="fa fa-download" + /> + </a> + </div> +</div> +`; + +exports[`ContentView Component should render correctly with empty content 1`] = ` +<div + className="alert alert-info" +> + No + response + content. +</div> +`; + +exports[`ContentView Component should render correctly with missing content 1`] = ` +<div + className="alert alert-info" +> + Response + content missing. +</div> +`; diff --git a/web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap b/web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap new file mode 100644 index 00000000..11c3a29e --- /dev/null +++ b/web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EventLog Component should connect to state and render correctly 1`] = ` +<div + className="eventlog" + style={ + Object { + "height": 200, + } + } +> + <div + onMouseDown={[Function]} + > + Eventlog + <div + className="pull-right" + > + <div + className="btn btn-toggle btn-primary" + onClick={[Function]} + > + <i + className="fa fa-fw fa-check-square-o" + /> + + debug + </div> + <div + className="btn btn-toggle btn-primary" + onClick={[Function]} + > + <i + className="fa fa-fw fa-check-square-o" + /> + + info + </div> + <div + className="btn btn-toggle btn-default" + onClick={[Function]} + > + <i + className="fa fa-fw fa-square-o" + /> + + web + </div> + <div + className="btn btn-toggle btn-primary" + onClick={[Function]} + > + <i + className="fa fa-fw fa-check-square-o" + /> + + warn + </div> + <div + className="btn btn-toggle btn-primary" + onClick={[Function]} + > + <i + className="fa fa-fw fa-check-square-o" + /> + + error + </div> + <i + className="fa fa-close" + onClick={[Function]} + /> + </div> + </div> +</div> +`; diff --git a/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap b/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap new file mode 100644 index 00000000..7149903c --- /dev/null +++ b/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlowTable Component should render correctly 1`] = ` +<div + className="flow-table" + onScroll={[Function]} +> + <table> + <thead + style={ + Object { + "transform": "translateY(undefinedpx)", + } + } + > + <tr> + <th + className="col-tls" + onClick={[Function]} + > + + </th> + <th + className="col-icon" + onClick={[Function]} + > + + </th> + <th + className="col-path sort-desc" + onClick={[Function]} + > + Path + </th> + <th + className="col-method" + onClick={[Function]} + > + Method + </th> + <th + className="col-status" + onClick={[Function]} + > + Status + </th> + <th + className="col-size" + onClick={[Function]} + > + Size + </th> + <th + className="col-time" + onClick={[Function]} + > + Time + </th> + </tr> + </thead> + <tbody> + <tr + style={ + Object { + "height": 0, + } + } + /> + <tr + style={ + Object { + "height": 0, + } + } + /> + </tbody> + </table> +</div> +`; diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx index a83cdb28..40fe900e 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -5,7 +5,7 @@ import { toggleFilter, toggleVisibility } from '../ducks/eventLog' import ToggleButton from './common/ToggleButton' import EventList from './EventLog/EventList' -class EventLog extends Component { +export class PureEventLog extends Component { static propTypes = { filters: PropTypes.object.isRequired, @@ -77,4 +77,4 @@ export default connect( close: toggleVisibility, toggleFilter: toggleFilter, } -)(EventLog) +)(PureEventLog) |