From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/server/actors/actor-registry.js | 54 + devtools/server/actors/addon.js | 352 +++ devtools/server/actors/addons.js | 41 + devtools/server/actors/animation.js | 751 +++++ devtools/server/actors/breakpoint.js | 189 ++ devtools/server/actors/call-watcher.js | 634 ++++ devtools/server/actors/canvas.js | 728 +++++ devtools/server/actors/child-process.js | 146 + devtools/server/actors/childtab.js | 82 + devtools/server/actors/chrome.js | 185 ++ devtools/server/actors/common.js | 521 ++++ devtools/server/actors/css-properties.js | 120 + devtools/server/actors/csscoverage.js | 726 +++++ devtools/server/actors/device.js | 70 + devtools/server/actors/director-manager.js | 615 ++++ devtools/server/actors/director-registry.js | 254 ++ devtools/server/actors/emulation.js | 241 ++ devtools/server/actors/environment.js | 199 ++ devtools/server/actors/errordocs.js | 84 + devtools/server/actors/eventlooplag.js | 60 + devtools/server/actors/frame.js | 100 + devtools/server/actors/framerate.js | 33 + devtools/server/actors/gcli.js | 233 ++ devtools/server/actors/heap-snapshot-file.js | 68 + devtools/server/actors/highlighters.css | 536 ++++ devtools/server/actors/highlighters.js | 715 +++++ .../server/actors/highlighters/auto-refresh.js | 215 ++ devtools/server/actors/highlighters/box-model.js | 712 +++++ devtools/server/actors/highlighters/css-grid.js | 737 +++++ .../server/actors/highlighters/css-transform.js | 243 ++ devtools/server/actors/highlighters/eye-dropper.js | 534 ++++ .../server/actors/highlighters/geometry-editor.js | 704 +++++ .../server/actors/highlighters/measuring-tool.js | 563 ++++ devtools/server/actors/highlighters/moz.build | 23 + devtools/server/actors/highlighters/rect.js | 102 + devtools/server/actors/highlighters/rulers.js | 294 ++ devtools/server/actors/highlighters/selector.js | 83 + .../server/actors/highlighters/simple-outline.js | 67 + .../server/actors/highlighters/utils/markup.js | 609 ++++ .../server/actors/highlighters/utils/moz.build | 9 + devtools/server/actors/inspector.js | 3186 ++++++++++++++++++++ devtools/server/actors/layout.js | 131 + devtools/server/actors/memory.js | 83 + devtools/server/actors/monitor.js | 145 + devtools/server/actors/moz.build | 69 + devtools/server/actors/object.js | 2251 ++++++++++++++ devtools/server/actors/performance-entries.js | 65 + devtools/server/actors/performance-recording.js | 148 + devtools/server/actors/performance.js | 116 + devtools/server/actors/preference.js | 81 + devtools/server/actors/pretty-print-worker.js | 50 + devtools/server/actors/process.js | 83 + devtools/server/actors/profiler.js | 60 + devtools/server/actors/promises.js | 200 ++ devtools/server/actors/reflow.js | 514 ++++ devtools/server/actors/root.js | 535 ++++ devtools/server/actors/script.js | 2360 +++++++++++++++ devtools/server/actors/settings.js | 146 + devtools/server/actors/source.js | 902 ++++++ devtools/server/actors/storage.js | 2542 ++++++++++++++++ devtools/server/actors/string.js | 43 + devtools/server/actors/styleeditor.js | 528 ++++ devtools/server/actors/styles.js | 1687 +++++++++++ devtools/server/actors/stylesheets.js | 982 ++++++ devtools/server/actors/timeline.js | 98 + devtools/server/actors/utils/TabSources.js | 833 +++++ .../server/actors/utils/actor-registry-utils.js | 78 + devtools/server/actors/utils/audionodes.json | 113 + .../server/actors/utils/automation-timeline.js | 373 +++ devtools/server/actors/utils/css-grid-utils.js | 61 + devtools/server/actors/utils/make-debugger.js | 101 + .../server/actors/utils/map-uri-to-addon-id.js | 44 + devtools/server/actors/utils/moz.build | 19 + devtools/server/actors/utils/stack.js | 185 ++ devtools/server/actors/utils/walker-search.js | 278 ++ devtools/server/actors/utils/webconsole-utils.js | 1063 +++++++ .../server/actors/utils/webconsole-worker-utils.js | 20 + devtools/server/actors/webaudio.js | 856 ++++++ devtools/server/actors/webbrowser.js | 2529 ++++++++++++++++ devtools/server/actors/webconsole.js | 2346 ++++++++++++++ devtools/server/actors/webextension.js | 333 ++ devtools/server/actors/webgl.js | 1322 ++++++++ devtools/server/actors/worker.js | 611 ++++ devtools/server/child.js | 127 + devtools/server/content-globals.js | 47 + devtools/server/content-server.jsm | 56 + devtools/server/css-logic.js | 1536 ++++++++++ devtools/server/docs/actor-e10s-handling.md | 106 + devtools/server/docs/actor-hierarchy.md | 129 + devtools/server/docs/actor-registration.md | 41 + devtools/server/docs/protocol.js.md | 651 ++++ devtools/server/event-parsers.js | 369 +++ devtools/server/main.js | 1902 ++++++++++++ devtools/server/moz.build | 42 + devtools/server/nsIJSInspector.idl | 75 + devtools/server/nsJSInspector.cpp | 147 + devtools/server/nsJSInspector.h | 39 + devtools/server/performance/framerate.js | 99 + devtools/server/performance/memory.js | 425 +++ devtools/server/performance/moz.build | 13 + devtools/server/performance/profiler.js | 546 ++++ devtools/server/performance/recorder.js | 494 +++ devtools/server/performance/timeline.js | 356 +++ devtools/server/primitive.js | 165 + devtools/server/service-worker-child.js | 32 + devtools/server/shims/moz.build | 18 + devtools/server/shims/protocol.js | 24 + devtools/server/shims/toolkit/dbg-server.jsm | 37 + devtools/server/shims/toolkit/moz.build | 17 + devtools/server/tests/browser/.eslintrc.js | 6 + devtools/server/tests/browser/animation.html | 170 ++ devtools/server/tests/browser/browser.ini | 97 + .../browser/browser_animation_emitMutations.js | 62 + .../tests/browser/browser_animation_getFrames.js | 32 + .../browser/browser_animation_getMultipleStates.js | 55 + .../tests/browser/browser_animation_getPlayers.js | 63 + .../browser/browser_animation_getProperties.js | 36 + .../browser_animation_getStateAfterFinished.js | 55 + .../browser_animation_getSubTreeAnimations.js | 38 + .../browser/browser_animation_keepFinished.js | 54 + .../browser/browser_animation_playPauseIframe.js | 51 + .../browser/browser_animation_playPauseSeveral.js | 92 + .../tests/browser/browser_animation_playerState.js | 123 + .../browser/browser_animation_reconstructState.js | 38 + .../browser_animation_refreshTransitions.js | 77 + .../browser/browser_animation_setCurrentTime.js | 74 + .../browser/browser_animation_setPlaybackRate.js | 51 + .../tests/browser/browser_animation_simple.js | 35 + .../browser/browser_animation_updatedState.js | 55 + .../tests/browser/browser_canvasframe_helper_01.js | 90 + .../tests/browser/browser_canvasframe_helper_02.js | 48 + .../tests/browser/browser_canvasframe_helper_03.js | 102 + .../tests/browser/browser_canvasframe_helper_04.js | 98 + .../tests/browser/browser_canvasframe_helper_05.js | 112 + .../tests/browser/browser_canvasframe_helper_06.js | 100 + .../tests/browser/browser_directorscript_actors.js | 159 + .../browser_directorscript_actors_error_events.js | 132 + .../browser_directorscript_actors_exports.js | 87 + .../browser/browser_markers-cycle-collection.js | 33 + .../tests/browser/browser_markers-docloading-01.js | 37 + .../tests/browser/browser_markers-docloading-02.js | 35 + .../tests/browser/browser_markers-docloading-03.js | 39 + .../server/tests/browser/browser_markers-gc.js | 50 + .../tests/browser/browser_markers-minor-gc.js | 32 + .../tests/browser/browser_markers-parse-html.js | 29 + .../server/tests/browser/browser_markers-styles.js | 34 + .../tests/browser/browser_markers-timestamp.js | 43 + .../server/tests/browser/browser_navigateEvents.js | 160 + .../tests/browser/browser_perf-allocation-data.js | 38 + .../tests/browser/browser_perf-profiler-01.js | 45 + .../tests/browser/browser_perf-profiler-02.js | 46 + .../tests/browser/browser_perf-profiler-03.js | 54 + .../tests/browser/browser_perf-realtime-markers.js | 93 + .../browser/browser_perf-recording-actor-01.js | 80 + .../browser/browser_perf-recording-actor-02.js | 54 + .../tests/browser/browser_perf-samples-01.js | 63 + .../tests/browser/browser_perf-samples-02.js | 77 + .../server/tests/browser/browser_register_actor.js | 76 + .../browser/browser_storage_dynamic_windows.js | 294 ++ .../tests/browser/browser_storage_listings.js | 610 ++++ .../tests/browser/browser_storage_updates.js | 304 ++ .../browser/browser_stylesheets_getTextEmpty.js | 40 + .../browser/browser_stylesheets_nested-iframes.js | 38 + devtools/server/tests/browser/browser_timeline.js | 63 + .../tests/browser/browser_timeline_actors.js | 69 + .../tests/browser/browser_timeline_iframes.js | 41 + .../tests/browser/director-script-target.html | 15 + devtools/server/tests/browser/doc_allocations.html | 21 + devtools/server/tests/browser/doc_force_cc.html | 29 + devtools/server/tests/browser/doc_force_gc.html | 27 + devtools/server/tests/browser/doc_innerHTML.html | 21 + devtools/server/tests/browser/doc_perf.html | 25 + devtools/server/tests/browser/head.js | 203 ++ devtools/server/tests/browser/navigate-first.html | 15 + devtools/server/tests/browser/navigate-second.html | 9 + .../tests/browser/storage-dynamic-windows.html | 117 + devtools/server/tests/browser/storage-helpers.js | 85 + .../server/tests/browser/storage-listings.html | 123 + .../tests/browser/storage-secured-iframe.html | 94 + .../tests/browser/storage-unsecured-iframe.html | 26 + devtools/server/tests/browser/storage-updates.html | 47 + .../tests/browser/stylesheets-nested-iframes.html | 25 + .../tests/browser/timeline-iframe-child.html | 19 + .../tests/browser/timeline-iframe-parent.html | 11 + devtools/server/tests/mochitest/.eslintrc.js | 6 + .../Debugger.Source.prototype.element-2.js | 1 + .../Debugger.Source.prototype.element.html | 17 + .../mochitest/Debugger.Source.prototype.element.js | 1 + .../server/tests/mochitest/animation-data.html | 120 + devtools/server/tests/mochitest/chrome.ini | 103 + .../server/tests/mochitest/director-helpers.js | 44 + devtools/server/tests/mochitest/hello-actor.js | 26 + .../mochitest/inspector-delay-image-response.sjs | 42 + .../tests/mochitest/inspector-eyedropper.html | 18 + .../server/tests/mochitest/inspector-helpers.js | 310 ++ .../tests/mochitest/inspector-search-data.html | 52 + .../tests/mochitest/inspector-styles-data.css | 3 + .../tests/mochitest/inspector-styles-data.html | 81 + .../tests/mochitest/inspector-traversal-data.html | 90 + .../tests/mochitest/inspector_css-properties.html | 10 + .../tests/mochitest/inspector_getImageData.html | 21 + devtools/server/tests/mochitest/large-image.jpg | Bin 0 -> 793541 bytes devtools/server/tests/mochitest/memory-helpers.js | 52 + .../mochitest/nonchrome_unsafeDereference.html | 8 + devtools/server/tests/mochitest/setup-in-child.js | 20 + devtools/server/tests/mochitest/setup-in-parent.js | 10 + devtools/server/tests/mochitest/small-image.gif | Bin 0 -> 510655 bytes .../test_Debugger.Script.prototype.global.html | 48 + .../test_Debugger.Source.prototype.element.html | 182 ++ ...bugger.Source.prototype.introductionScript.html | 97 + ...Debugger.Source.prototype.introductionType.html | 181 ++ .../mochitest/test_animation_actor-lifetime.html | 91 + .../tests/mochitest/test_connectToChild.html | 134 + .../tests/mochitest/test_connection-manager.html | 119 + .../mochitest/test_css-logic-media-queries.html | 62 + .../mochitest/test_css-logic-specificity.html | 84 + .../server/tests/mochitest/test_css-logic.html | 167 + .../tests/mochitest/test_css-properties_01.html | 121 + .../tests/mochitest/test_css-properties_02.html | 86 + devtools/server/tests/mochitest/test_device.html | 99 + devtools/server/tests/mochitest/test_director.html | 114 + .../mochitest/test_director_connectToChild.html | 98 + .../test_executeInGlobal-outerized_this.html | 69 + .../server/tests/mochitest/test_framerate_01.html | 141 + .../server/tests/mochitest/test_framerate_02.html | 113 + .../server/tests/mochitest/test_framerate_03.html | 82 + .../server/tests/mochitest/test_framerate_04.html | 72 + .../server/tests/mochitest/test_framerate_05.html | 77 + .../server/tests/mochitest/test_framerate_06.html | 82 + .../server/tests/mochitest/test_getProcess.html | 120 + .../tests/mochitest/test_inspector-anonymous.html | 201 ++ .../mochitest/test_inspector-changeattrs.html | 99 + .../mochitest/test_inspector-changevalue.html | 82 + .../tests/mochitest/test_inspector-dead-nodes.html | 386 +++ .../mochitest/test_inspector-duplicate-node.html | 75 + .../tests/mochitest/test_inspector-hide.html | 76 + .../tests/mochitest/test_inspector-insert.html | 119 + .../mochitest/test_inspector-mutations-attr.html | 167 + .../test_inspector-mutations-childlist.html | 310 ++ .../mochitest/test_inspector-mutations-events.html | 183 ++ .../test_inspector-mutations-frameload.html | 214 ++ .../mochitest/test_inspector-mutations-value.html | 168 ++ .../tests/mochitest/test_inspector-pick-color.html | 101 + .../mochitest/test_inspector-pseudoclass-lock.html | 174 ++ .../tests/mochitest/test_inspector-release.html | 102 + .../tests/mochitest/test_inspector-reload.html | 80 + .../tests/mochitest/test_inspector-remove.html | 117 + .../tests/mochitest/test_inspector-resize.html | 77 + .../mochitest/test_inspector-resolve-url.html | 88 + .../tests/mochitest/test_inspector-retain.html | 180 ++ .../mochitest/test_inspector-scroll-into-view.html | 87 + .../mochitest/test_inspector-search-front.html | 217 ++ .../tests/mochitest/test_inspector-search.html | 296 ++ .../tests/mochitest/test_inspector-traversal.html | 354 +++ .../test_inspector_getImageData-wait-for-load.html | 136 + .../mochitest/test_inspector_getImageData.html | 166 + .../test_inspector_getImageDataFromURL.html | 114 + .../mochitest/test_inspector_getNodeFromActor.html | 88 + .../mochitest/test_makeGlobalObjectReference.html | 86 + devtools/server/tests/mochitest/test_memory.html | 37 + .../mochitest/test_memory_allocations_01.html | 98 + .../mochitest/test_memory_allocations_02.html | 76 + .../mochitest/test_memory_allocations_03.html | 78 + .../mochitest/test_memory_allocations_04.html | 60 + .../mochitest/test_memory_allocations_05.html | 87 + .../mochitest/test_memory_allocations_06.html | 49 + .../mochitest/test_memory_allocations_07.html | 55 + .../tests/mochitest/test_memory_attach_01.html | 31 + .../tests/mochitest/test_memory_attach_02.html | 49 + .../server/tests/mochitest/test_memory_census.html | 33 + .../server/tests/mochitest/test_memory_gc_01.html | 46 + .../tests/mochitest/test_memory_gc_events.html | 42 + .../server/tests/mochitest/test_preference.html | 115 + devtools/server/tests/mochitest/test_settings.html | 130 + .../tests/mochitest/test_setupInParentChild.html | 110 + .../tests/mochitest/test_styles-applied.html | 145 + .../tests/mochitest/test_styles-computed.html | 139 + .../server/tests/mochitest/test_styles-layout.html | 116 + .../tests/mochitest/test_styles-matched.html | 98 + .../server/tests/mochitest/test_styles-modify.html | 116 + .../server/tests/mochitest/test_styles-svg.html | 70 + .../tests/mochitest/test_unsafeDereference.html | 52 + .../tests/mochitest/test_websocket-server.html | 82 + devtools/server/tests/unit/.eslintrc.js | 6 + .../addons/web-extension-upgrade/manifest.json | 10 + .../tests/unit/addons/web-extension/manifest.json | 10 + .../tests/unit/addons/web-extension2/manifest.json | 10 + .../babel_and_browserify_script_with_source_map.js | 79 + devtools/server/tests/unit/head_dbg.js | 862 ++++++ devtools/server/tests/unit/hello-actor.js | 18 + .../server/tests/unit/post_init_global_actors.js | 17 + devtools/server/tests/unit/post_init_tab_actors.js | 17 + .../server/tests/unit/pre_init_global_actors.js | 17 + devtools/server/tests/unit/pre_init_tab_actors.js | 17 + .../server/tests/unit/registertestactors-01.js | 15 + .../server/tests/unit/registertestactors-02.js | 15 + .../server/tests/unit/registertestactors-03.js | 40 + .../unit/setBreakpoint-on-column-in-gcd-script.js | 7 + ...int-on-column-with-no-offsets-at-end-of-line.js | 6 + ...oint-on-column-with-no-offsets-in-gcd-script.js | 7 + .../setBreakpoint-on-column-with-no-offsets.js | 5 + .../server/tests/unit/setBreakpoint-on-column.js | 5 + .../unit/setBreakpoint-on-line-in-gcd-script.js | 9 + .../setBreakpoint-on-line-with-multiple-offsets.js | 7 + ...tBreakpoint-on-line-with-multiple-statements.js | 5 + ...kpoint-on-line-with-no-offsets-in-gcd-script.js | 9 + .../unit/setBreakpoint-on-line-with-no-offsets.js | 7 + .../server/tests/unit/setBreakpoint-on-line.js | 7 + .../tests/unit/source-map-data/sourcemapped.coffee | 6 + .../tests/unit/source-map-data/sourcemapped.map | 10 + devtools/server/tests/unit/sourcemapped.js | 16 + .../unit/test_MemoryActor_saveHeapSnapshot_01.js | 18 + .../unit/test_MemoryActor_saveHeapSnapshot_02.js | 20 + .../unit/test_MemoryActor_saveHeapSnapshot_03.js | 18 + .../server/tests/unit/test_actor-registry-actor.js | 80 + devtools/server/tests/unit/test_add_actors.js | 107 + devtools/server/tests/unit/test_addon_reload.js | 98 + devtools/server/tests/unit/test_addons_actor.js | 51 + devtools/server/tests/unit/test_animation_name.js | 87 + devtools/server/tests/unit/test_animation_type.js | 68 + devtools/server/tests/unit/test_attach.js | 37 + devtools/server/tests/unit/test_blackboxing-01.js | 145 + devtools/server/tests/unit/test_blackboxing-02.js | 113 + devtools/server/tests/unit/test_blackboxing-03.js | 102 + devtools/server/tests/unit/test_blackboxing-04.js | 88 + devtools/server/tests/unit/test_blackboxing-05.js | 84 + devtools/server/tests/unit/test_blackboxing-06.js | 102 + devtools/server/tests/unit/test_blackboxing-07.js | 62 + devtools/server/tests/unit/test_breakpoint-01.js | 75 + devtools/server/tests/unit/test_breakpoint-02.js | 67 + devtools/server/tests/unit/test_breakpoint-03.js | 96 + devtools/server/tests/unit/test_breakpoint-04.js | 80 + devtools/server/tests/unit/test_breakpoint-05.js | 82 + devtools/server/tests/unit/test_breakpoint-06.js | 89 + devtools/server/tests/unit/test_breakpoint-07.js | 85 + devtools/server/tests/unit/test_breakpoint-08.js | 96 + devtools/server/tests/unit/test_breakpoint-09.js | 88 + devtools/server/tests/unit/test_breakpoint-10.js | 89 + devtools/server/tests/unit/test_breakpoint-11.js | 88 + devtools/server/tests/unit/test_breakpoint-12.js | 113 + devtools/server/tests/unit/test_breakpoint-13.js | 115 + devtools/server/tests/unit/test_breakpoint-14.js | 113 + devtools/server/tests/unit/test_breakpoint-15.js | 69 + devtools/server/tests/unit/test_breakpoint-16.js | 83 + devtools/server/tests/unit/test_breakpoint-17.js | 120 + devtools/server/tests/unit/test_breakpoint-18.js | 82 + devtools/server/tests/unit/test_breakpoint-19.js | 70 + devtools/server/tests/unit/test_breakpoint-20.js | 108 + devtools/server/tests/unit/test_breakpoint-21.js | 85 + .../server/tests/unit/test_breakpoint-actor-map.js | 180 ++ devtools/server/tests/unit/test_client_close.js | 39 + devtools/server/tests/unit/test_client_request.js | 214 ++ .../tests/unit/test_conditional_breakpoint-01.js | 61 + .../tests/unit/test_conditional_breakpoint-02.js | 60 + .../tests/unit/test_conditional_breakpoint-03.js | 61 + devtools/server/tests/unit/test_dbgactor.js | 116 + .../tests/unit/test_dbgclient_debuggerstatement.js | 73 + devtools/server/tests/unit/test_dbgglobal.js | 62 + devtools/server/tests/unit/test_eval-01.js | 58 + devtools/server/tests/unit/test_eval-02.js | 48 + devtools/server/tests/unit/test_eval-03.js | 50 + devtools/server/tests/unit/test_eval-04.js | 69 + devtools/server/tests/unit/test_eval-05.js | 54 + .../server/tests/unit/test_eventlooplag_actor.js | 59 + .../server/tests/unit/test_forwardingprefix.js | 196 ++ devtools/server/tests/unit/test_frameactor-01.js | 43 + devtools/server/tests/unit/test_frameactor-02.js | 45 + devtools/server/tests/unit/test_frameactor-03.js | 47 + devtools/server/tests/unit/test_frameactor-04.js | 91 + devtools/server/tests/unit/test_frameactor-05.js | 89 + .../server/tests/unit/test_framearguments-01.js | 51 + .../server/tests/unit/test_framebindings-01.js | 77 + .../server/tests/unit/test_framebindings-02.js | 63 + .../server/tests/unit/test_framebindings-03.js | 69 + .../server/tests/unit/test_framebindings-04.js | 84 + .../server/tests/unit/test_framebindings-05.js | 63 + .../server/tests/unit/test_framebindings-06.js | 60 + .../server/tests/unit/test_framebindings-07.js | 64 + devtools/server/tests/unit/test_frameclient-01.js | 53 + devtools/server/tests/unit/test_frameclient-02.js | 46 + .../server/tests/unit/test_functiongrips-01.js | 95 + .../unit/test_get-executable-lines-source-map.js | 56 + .../server/tests/unit/test_get-executable-lines.js | 55 + devtools/server/tests/unit/test_getRuleText.js | 137 + .../server/tests/unit/test_getTextAtLineColumn.js | 35 + .../server/tests/unit/test_getyoungestframe.js | 30 + .../tests/unit/test_ignore_caught_exceptions.js | 50 + .../unit/test_ignore_no_interface_exceptions.js | 54 + devtools/server/tests/unit/test_interrupt.js | 50 + .../tests/unit/test_layout-reflows-observer.js | 286 ++ devtools/server/tests/unit/test_listsources-01.js | 59 + devtools/server/tests/unit/test_listsources-02.js | 49 + devtools/server/tests/unit/test_listsources-03.js | 52 + devtools/server/tests/unit/test_listsources-04.js | 58 + devtools/server/tests/unit/test_longstringactor.js | 104 + .../server/tests/unit/test_longstringgrips-01.js | 71 + .../server/tests/unit/test_longstringgrips-02.js | 60 + devtools/server/tests/unit/test_monitor_actor.js | 76 + devtools/server/tests/unit/test_nativewrappers.js | 30 + devtools/server/tests/unit/test_nesting-01.js | 48 + devtools/server/tests/unit/test_nesting-02.js | 81 + devtools/server/tests/unit/test_nesting-03.js | 51 + devtools/server/tests/unit/test_new_source-01.js | 40 + devtools/server/tests/unit/test_nodelistactor.js | 26 + devtools/server/tests/unit/test_nsjsinspector.js | 59 + devtools/server/tests/unit/test_objectgrips-01.js | 58 + devtools/server/tests/unit/test_objectgrips-02.js | 65 + devtools/server/tests/unit/test_objectgrips-03.js | 73 + devtools/server/tests/unit/test_objectgrips-04.js | 76 + devtools/server/tests/unit/test_objectgrips-05.js | 66 + devtools/server/tests/unit/test_objectgrips-06.js | 66 + devtools/server/tests/unit/test_objectgrips-07.js | 74 + devtools/server/tests/unit/test_objectgrips-08.js | 72 + devtools/server/tests/unit/test_objectgrips-09.js | 74 + devtools/server/tests/unit/test_objectgrips-10.js | 72 + devtools/server/tests/unit/test_objectgrips-11.js | 52 + devtools/server/tests/unit/test_objectgrips-12.js | 162 + devtools/server/tests/unit/test_objectgrips-13.js | 66 + .../server/tests/unit/test_pause_exceptions-01.js | 50 + .../server/tests/unit/test_pause_exceptions-02.js | 47 + .../server/tests/unit/test_pauselifetime-01.js | 54 + .../server/tests/unit/test_pauselifetime-02.js | 56 + .../server/tests/unit/test_pauselifetime-03.js | 61 + .../server/tests/unit/test_pauselifetime-04.js | 48 + .../tests/unit/test_profiler_activation-01.js | 89 + .../tests/unit/test_profiler_activation-02.js | 46 + .../tests/unit/test_profiler_bufferstatus.js | 127 + devtools/server/tests/unit/test_profiler_close.js | 69 + devtools/server/tests/unit/test_profiler_data.js | 110 + .../server/tests/unit/test_profiler_events-01.js | 62 + .../server/tests/unit/test_profiler_events-02.js | 70 + .../tests/unit/test_profiler_getbufferinfo.js | 123 + .../server/tests/unit/test_profiler_getfeatures.js | 35 + .../test_profiler_getsharedlibraryinformation.js | 45 + .../server/tests/unit/test_promise_state-01.js | 40 + .../server/tests/unit/test_promise_state-02.js | 45 + .../server/tests/unit/test_promise_state-03.js | 45 + .../tests/unit/test_promises_actor_attach.js | 52 + .../server/tests/unit/test_promises_actor_exist.js | 29 + .../unit/test_promises_actor_list_promises.js | 63 + .../tests/unit/test_promises_actor_onnewpromise.js | 72 + .../unit/test_promises_actor_onpromisesettled.js | 92 + .../test_promises_client_getdependentpromises.js | 112 + .../unit/test_promises_object_creationtimestamp.js | 71 + .../unit/test_promises_object_timetosettle-01.js | 80 + .../unit/test_promises_object_timetosettle-02.js | 74 + devtools/server/tests/unit/test_protocolSpec.js | 17 + devtools/server/tests/unit/test_protocol_abort.js | 83 + devtools/server/tests/unit/test_protocol_async.js | 184 ++ .../server/tests/unit/test_protocol_children.js | 559 ++++ .../server/tests/unit/test_protocol_formtype.js | 177 ++ .../server/tests/unit/test_protocol_longstring.js | 218 ++ devtools/server/tests/unit/test_protocol_simple.js | 319 ++ devtools/server/tests/unit/test_protocol_stack.js | 98 + .../server/tests/unit/test_protocol_unregister.js | 44 + devtools/server/tests/unit/test_reattach-thread.js | 58 + devtools/server/tests/unit/test_registerClient.js | 95 + devtools/server/tests/unit/test_register_actor.js | 113 + devtools/server/tests/unit/test_requestTypes.js | 36 + devtools/server/tests/unit/test_safe-getter.js | 25 + .../test_setBreakpoint-on-column-in-gcd-script.js | 58 + ...int-on-column-with-no-offsets-at-end-of-line.js | 39 + .../tests/unit/test_setBreakpoint-on-column.js | 57 + .../test_setBreakpoint-on-line-in-gcd-script.js | 57 + ..._setBreakpoint-on-line-with-multiple-offsets.js | 70 + ...tBreakpoint-on-line-with-multiple-statements.js | 57 + ...kpoint-on-line-with-no-offsets-in-gcd-script.js | 58 + .../test_setBreakpoint-on-line-with-no-offsets.js | 57 + .../tests/unit/test_setBreakpoint-on-line.js | 57 + devtools/server/tests/unit/test_source-01.js | 78 + devtools/server/tests/unit/test_sourcemaps-01.js | 64 + devtools/server/tests/unit/test_sourcemaps-02.js | 67 + devtools/server/tests/unit/test_sourcemaps-03.js | 137 + devtools/server/tests/unit/test_sourcemaps-04.js | 46 + devtools/server/tests/unit/test_sourcemaps-05.js | 46 + devtools/server/tests/unit/test_sourcemaps-06.js | 94 + devtools/server/tests/unit/test_sourcemaps-07.js | 67 + devtools/server/tests/unit/test_sourcemaps-08.js | 50 + devtools/server/tests/unit/test_sourcemaps-09.js | 95 + devtools/server/tests/unit/test_sourcemaps-10.js | 73 + devtools/server/tests/unit/test_sourcemaps-11.js | 83 + devtools/server/tests/unit/test_sourcemaps-12.js | 75 + devtools/server/tests/unit/test_sourcemaps-13.js | 105 + devtools/server/tests/unit/test_sourcemaps-16.js | 46 + devtools/server/tests/unit/test_sourcemaps-17.js | 63 + devtools/server/tests/unit/test_stepping-01.js | 83 + devtools/server/tests/unit/test_stepping-02.js | 83 + devtools/server/tests/unit/test_stepping-03.js | 62 + devtools/server/tests/unit/test_stepping-04.js | 74 + devtools/server/tests/unit/test_stepping-05.js | 101 + devtools/server/tests/unit/test_stepping-06.js | 99 + devtools/server/tests/unit/test_stepping-07.js | 92 + devtools/server/tests/unit/test_symbols-01.js | 58 + devtools/server/tests/unit/test_symbols-02.js | 49 + .../server/tests/unit/test_threadlifetime-01.js | 58 + .../server/tests/unit/test_threadlifetime-02.js | 59 + .../server/tests/unit/test_threadlifetime-03.js | 81 + .../server/tests/unit/test_threadlifetime-04.js | 53 + .../server/tests/unit/test_threadlifetime-05.js | 83 + .../server/tests/unit/test_threadlifetime-06.js | 71 + .../server/tests/unit/test_unsafeDereference.js | 134 + .../server/tests/unit/test_xpcshell_debugging.js | 48 + devtools/server/tests/unit/testactors.js | 176 ++ devtools/server/tests/unit/tracerlocations.js | 8 + devtools/server/tests/unit/xpcshell.ini | 232 ++ .../server/tests/unit/xpcshell_debugging_script.js | 9 + devtools/server/websocket-server.js | 221 ++ devtools/server/worker.js | 110 + 508 files changed, 80988 insertions(+) create mode 100644 devtools/server/actors/actor-registry.js create mode 100644 devtools/server/actors/addon.js create mode 100644 devtools/server/actors/addons.js create mode 100644 devtools/server/actors/animation.js create mode 100644 devtools/server/actors/breakpoint.js create mode 100644 devtools/server/actors/call-watcher.js create mode 100644 devtools/server/actors/canvas.js create mode 100644 devtools/server/actors/child-process.js create mode 100644 devtools/server/actors/childtab.js create mode 100644 devtools/server/actors/chrome.js create mode 100644 devtools/server/actors/common.js create mode 100644 devtools/server/actors/css-properties.js create mode 100644 devtools/server/actors/csscoverage.js create mode 100644 devtools/server/actors/device.js create mode 100644 devtools/server/actors/director-manager.js create mode 100644 devtools/server/actors/director-registry.js create mode 100644 devtools/server/actors/emulation.js create mode 100644 devtools/server/actors/environment.js create mode 100644 devtools/server/actors/errordocs.js create mode 100644 devtools/server/actors/eventlooplag.js create mode 100644 devtools/server/actors/frame.js create mode 100644 devtools/server/actors/framerate.js create mode 100644 devtools/server/actors/gcli.js create mode 100644 devtools/server/actors/heap-snapshot-file.js create mode 100644 devtools/server/actors/highlighters.css create mode 100644 devtools/server/actors/highlighters.js create mode 100644 devtools/server/actors/highlighters/auto-refresh.js create mode 100644 devtools/server/actors/highlighters/box-model.js create mode 100644 devtools/server/actors/highlighters/css-grid.js create mode 100644 devtools/server/actors/highlighters/css-transform.js create mode 100644 devtools/server/actors/highlighters/eye-dropper.js create mode 100644 devtools/server/actors/highlighters/geometry-editor.js create mode 100644 devtools/server/actors/highlighters/measuring-tool.js create mode 100644 devtools/server/actors/highlighters/moz.build create mode 100644 devtools/server/actors/highlighters/rect.js create mode 100644 devtools/server/actors/highlighters/rulers.js create mode 100644 devtools/server/actors/highlighters/selector.js create mode 100644 devtools/server/actors/highlighters/simple-outline.js create mode 100644 devtools/server/actors/highlighters/utils/markup.js create mode 100644 devtools/server/actors/highlighters/utils/moz.build create mode 100644 devtools/server/actors/inspector.js create mode 100644 devtools/server/actors/layout.js create mode 100644 devtools/server/actors/memory.js create mode 100644 devtools/server/actors/monitor.js create mode 100644 devtools/server/actors/moz.build create mode 100644 devtools/server/actors/object.js create mode 100644 devtools/server/actors/performance-entries.js create mode 100644 devtools/server/actors/performance-recording.js create mode 100644 devtools/server/actors/performance.js create mode 100644 devtools/server/actors/preference.js create mode 100644 devtools/server/actors/pretty-print-worker.js create mode 100644 devtools/server/actors/process.js create mode 100644 devtools/server/actors/profiler.js create mode 100644 devtools/server/actors/promises.js create mode 100644 devtools/server/actors/reflow.js create mode 100644 devtools/server/actors/root.js create mode 100644 devtools/server/actors/script.js create mode 100644 devtools/server/actors/settings.js create mode 100644 devtools/server/actors/source.js create mode 100644 devtools/server/actors/storage.js create mode 100644 devtools/server/actors/string.js create mode 100644 devtools/server/actors/styleeditor.js create mode 100644 devtools/server/actors/styles.js create mode 100644 devtools/server/actors/stylesheets.js create mode 100644 devtools/server/actors/timeline.js create mode 100644 devtools/server/actors/utils/TabSources.js create mode 100644 devtools/server/actors/utils/actor-registry-utils.js create mode 100644 devtools/server/actors/utils/audionodes.json create mode 100644 devtools/server/actors/utils/automation-timeline.js create mode 100644 devtools/server/actors/utils/css-grid-utils.js create mode 100644 devtools/server/actors/utils/make-debugger.js create mode 100644 devtools/server/actors/utils/map-uri-to-addon-id.js create mode 100644 devtools/server/actors/utils/moz.build create mode 100644 devtools/server/actors/utils/stack.js create mode 100644 devtools/server/actors/utils/walker-search.js create mode 100644 devtools/server/actors/utils/webconsole-utils.js create mode 100644 devtools/server/actors/utils/webconsole-worker-utils.js create mode 100644 devtools/server/actors/webaudio.js create mode 100644 devtools/server/actors/webbrowser.js create mode 100644 devtools/server/actors/webconsole.js create mode 100644 devtools/server/actors/webextension.js create mode 100644 devtools/server/actors/webgl.js create mode 100644 devtools/server/actors/worker.js create mode 100644 devtools/server/child.js create mode 100644 devtools/server/content-globals.js create mode 100644 devtools/server/content-server.jsm create mode 100644 devtools/server/css-logic.js create mode 100644 devtools/server/docs/actor-e10s-handling.md create mode 100644 devtools/server/docs/actor-hierarchy.md create mode 100644 devtools/server/docs/actor-registration.md create mode 100644 devtools/server/docs/protocol.js.md create mode 100644 devtools/server/event-parsers.js create mode 100644 devtools/server/main.js create mode 100644 devtools/server/moz.build create mode 100644 devtools/server/nsIJSInspector.idl create mode 100644 devtools/server/nsJSInspector.cpp create mode 100644 devtools/server/nsJSInspector.h create mode 100644 devtools/server/performance/framerate.js create mode 100644 devtools/server/performance/memory.js create mode 100644 devtools/server/performance/moz.build create mode 100644 devtools/server/performance/profiler.js create mode 100644 devtools/server/performance/recorder.js create mode 100644 devtools/server/performance/timeline.js create mode 100644 devtools/server/primitive.js create mode 100644 devtools/server/service-worker-child.js create mode 100644 devtools/server/shims/moz.build create mode 100644 devtools/server/shims/protocol.js create mode 100644 devtools/server/shims/toolkit/dbg-server.jsm create mode 100644 devtools/server/shims/toolkit/moz.build create mode 100644 devtools/server/tests/browser/.eslintrc.js create mode 100644 devtools/server/tests/browser/animation.html create mode 100644 devtools/server/tests/browser/browser.ini create mode 100644 devtools/server/tests/browser/browser_animation_emitMutations.js create mode 100644 devtools/server/tests/browser/browser_animation_getFrames.js create mode 100644 devtools/server/tests/browser/browser_animation_getMultipleStates.js create mode 100644 devtools/server/tests/browser/browser_animation_getPlayers.js create mode 100644 devtools/server/tests/browser/browser_animation_getProperties.js create mode 100644 devtools/server/tests/browser/browser_animation_getStateAfterFinished.js create mode 100644 devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js create mode 100644 devtools/server/tests/browser/browser_animation_keepFinished.js create mode 100644 devtools/server/tests/browser/browser_animation_playPauseIframe.js create mode 100644 devtools/server/tests/browser/browser_animation_playPauseSeveral.js create mode 100644 devtools/server/tests/browser/browser_animation_playerState.js create mode 100644 devtools/server/tests/browser/browser_animation_reconstructState.js create mode 100644 devtools/server/tests/browser/browser_animation_refreshTransitions.js create mode 100644 devtools/server/tests/browser/browser_animation_setCurrentTime.js create mode 100644 devtools/server/tests/browser/browser_animation_setPlaybackRate.js create mode 100644 devtools/server/tests/browser/browser_animation_simple.js create mode 100644 devtools/server/tests/browser/browser_animation_updatedState.js create mode 100644 devtools/server/tests/browser/browser_canvasframe_helper_01.js create mode 100644 devtools/server/tests/browser/browser_canvasframe_helper_02.js create mode 100644 devtools/server/tests/browser/browser_canvasframe_helper_03.js create mode 100644 devtools/server/tests/browser/browser_canvasframe_helper_04.js create mode 100644 devtools/server/tests/browser/browser_canvasframe_helper_05.js create mode 100644 devtools/server/tests/browser/browser_canvasframe_helper_06.js create mode 100644 devtools/server/tests/browser/browser_directorscript_actors.js create mode 100644 devtools/server/tests/browser/browser_directorscript_actors_error_events.js create mode 100644 devtools/server/tests/browser/browser_directorscript_actors_exports.js create mode 100644 devtools/server/tests/browser/browser_markers-cycle-collection.js create mode 100644 devtools/server/tests/browser/browser_markers-docloading-01.js create mode 100644 devtools/server/tests/browser/browser_markers-docloading-02.js create mode 100644 devtools/server/tests/browser/browser_markers-docloading-03.js create mode 100644 devtools/server/tests/browser/browser_markers-gc.js create mode 100644 devtools/server/tests/browser/browser_markers-minor-gc.js create mode 100644 devtools/server/tests/browser/browser_markers-parse-html.js create mode 100644 devtools/server/tests/browser/browser_markers-styles.js create mode 100644 devtools/server/tests/browser/browser_markers-timestamp.js create mode 100644 devtools/server/tests/browser/browser_navigateEvents.js create mode 100644 devtools/server/tests/browser/browser_perf-allocation-data.js create mode 100644 devtools/server/tests/browser/browser_perf-profiler-01.js create mode 100644 devtools/server/tests/browser/browser_perf-profiler-02.js create mode 100644 devtools/server/tests/browser/browser_perf-profiler-03.js create mode 100644 devtools/server/tests/browser/browser_perf-realtime-markers.js create mode 100644 devtools/server/tests/browser/browser_perf-recording-actor-01.js create mode 100644 devtools/server/tests/browser/browser_perf-recording-actor-02.js create mode 100644 devtools/server/tests/browser/browser_perf-samples-01.js create mode 100644 devtools/server/tests/browser/browser_perf-samples-02.js create mode 100644 devtools/server/tests/browser/browser_register_actor.js create mode 100644 devtools/server/tests/browser/browser_storage_dynamic_windows.js create mode 100644 devtools/server/tests/browser/browser_storage_listings.js create mode 100644 devtools/server/tests/browser/browser_storage_updates.js create mode 100644 devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js create mode 100644 devtools/server/tests/browser/browser_stylesheets_nested-iframes.js create mode 100644 devtools/server/tests/browser/browser_timeline.js create mode 100644 devtools/server/tests/browser/browser_timeline_actors.js create mode 100644 devtools/server/tests/browser/browser_timeline_iframes.js create mode 100644 devtools/server/tests/browser/director-script-target.html create mode 100644 devtools/server/tests/browser/doc_allocations.html create mode 100644 devtools/server/tests/browser/doc_force_cc.html create mode 100644 devtools/server/tests/browser/doc_force_gc.html create mode 100644 devtools/server/tests/browser/doc_innerHTML.html create mode 100644 devtools/server/tests/browser/doc_perf.html create mode 100644 devtools/server/tests/browser/head.js create mode 100644 devtools/server/tests/browser/navigate-first.html create mode 100644 devtools/server/tests/browser/navigate-second.html create mode 100644 devtools/server/tests/browser/storage-dynamic-windows.html create mode 100644 devtools/server/tests/browser/storage-helpers.js create mode 100644 devtools/server/tests/browser/storage-listings.html create mode 100644 devtools/server/tests/browser/storage-secured-iframe.html create mode 100644 devtools/server/tests/browser/storage-unsecured-iframe.html create mode 100644 devtools/server/tests/browser/storage-updates.html create mode 100644 devtools/server/tests/browser/stylesheets-nested-iframes.html create mode 100644 devtools/server/tests/browser/timeline-iframe-child.html create mode 100644 devtools/server/tests/browser/timeline-iframe-parent.html create mode 100644 devtools/server/tests/mochitest/.eslintrc.js create mode 100644 devtools/server/tests/mochitest/Debugger.Source.prototype.element-2.js create mode 100644 devtools/server/tests/mochitest/Debugger.Source.prototype.element.html create mode 100644 devtools/server/tests/mochitest/Debugger.Source.prototype.element.js create mode 100644 devtools/server/tests/mochitest/animation-data.html create mode 100644 devtools/server/tests/mochitest/chrome.ini create mode 100644 devtools/server/tests/mochitest/director-helpers.js create mode 100644 devtools/server/tests/mochitest/hello-actor.js create mode 100644 devtools/server/tests/mochitest/inspector-delay-image-response.sjs create mode 100644 devtools/server/tests/mochitest/inspector-eyedropper.html create mode 100644 devtools/server/tests/mochitest/inspector-helpers.js create mode 100644 devtools/server/tests/mochitest/inspector-search-data.html create mode 100644 devtools/server/tests/mochitest/inspector-styles-data.css create mode 100644 devtools/server/tests/mochitest/inspector-styles-data.html create mode 100644 devtools/server/tests/mochitest/inspector-traversal-data.html create mode 100644 devtools/server/tests/mochitest/inspector_css-properties.html create mode 100644 devtools/server/tests/mochitest/inspector_getImageData.html create mode 100644 devtools/server/tests/mochitest/large-image.jpg create mode 100644 devtools/server/tests/mochitest/memory-helpers.js create mode 100644 devtools/server/tests/mochitest/nonchrome_unsafeDereference.html create mode 100644 devtools/server/tests/mochitest/setup-in-child.js create mode 100644 devtools/server/tests/mochitest/setup-in-parent.js create mode 100644 devtools/server/tests/mochitest/small-image.gif create mode 100644 devtools/server/tests/mochitest/test_Debugger.Script.prototype.global.html create mode 100644 devtools/server/tests/mochitest/test_Debugger.Source.prototype.element.html create mode 100644 devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionScript.html create mode 100644 devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionType.html create mode 100644 devtools/server/tests/mochitest/test_animation_actor-lifetime.html create mode 100644 devtools/server/tests/mochitest/test_connectToChild.html create mode 100644 devtools/server/tests/mochitest/test_connection-manager.html create mode 100644 devtools/server/tests/mochitest/test_css-logic-media-queries.html create mode 100644 devtools/server/tests/mochitest/test_css-logic-specificity.html create mode 100644 devtools/server/tests/mochitest/test_css-logic.html create mode 100644 devtools/server/tests/mochitest/test_css-properties_01.html create mode 100644 devtools/server/tests/mochitest/test_css-properties_02.html create mode 100644 devtools/server/tests/mochitest/test_device.html create mode 100644 devtools/server/tests/mochitest/test_director.html create mode 100644 devtools/server/tests/mochitest/test_director_connectToChild.html create mode 100644 devtools/server/tests/mochitest/test_executeInGlobal-outerized_this.html create mode 100644 devtools/server/tests/mochitest/test_framerate_01.html create mode 100644 devtools/server/tests/mochitest/test_framerate_02.html create mode 100644 devtools/server/tests/mochitest/test_framerate_03.html create mode 100644 devtools/server/tests/mochitest/test_framerate_04.html create mode 100644 devtools/server/tests/mochitest/test_framerate_05.html create mode 100644 devtools/server/tests/mochitest/test_framerate_06.html create mode 100644 devtools/server/tests/mochitest/test_getProcess.html create mode 100644 devtools/server/tests/mochitest/test_inspector-anonymous.html create mode 100644 devtools/server/tests/mochitest/test_inspector-changeattrs.html create mode 100644 devtools/server/tests/mochitest/test_inspector-changevalue.html create mode 100644 devtools/server/tests/mochitest/test_inspector-dead-nodes.html create mode 100644 devtools/server/tests/mochitest/test_inspector-duplicate-node.html create mode 100644 devtools/server/tests/mochitest/test_inspector-hide.html create mode 100644 devtools/server/tests/mochitest/test_inspector-insert.html create mode 100644 devtools/server/tests/mochitest/test_inspector-mutations-attr.html create mode 100644 devtools/server/tests/mochitest/test_inspector-mutations-childlist.html create mode 100644 devtools/server/tests/mochitest/test_inspector-mutations-events.html create mode 100644 devtools/server/tests/mochitest/test_inspector-mutations-frameload.html create mode 100644 devtools/server/tests/mochitest/test_inspector-mutations-value.html create mode 100644 devtools/server/tests/mochitest/test_inspector-pick-color.html create mode 100644 devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html create mode 100644 devtools/server/tests/mochitest/test_inspector-release.html create mode 100644 devtools/server/tests/mochitest/test_inspector-reload.html create mode 100644 devtools/server/tests/mochitest/test_inspector-remove.html create mode 100644 devtools/server/tests/mochitest/test_inspector-resize.html create mode 100644 devtools/server/tests/mochitest/test_inspector-resolve-url.html create mode 100644 devtools/server/tests/mochitest/test_inspector-retain.html create mode 100644 devtools/server/tests/mochitest/test_inspector-scroll-into-view.html create mode 100644 devtools/server/tests/mochitest/test_inspector-search-front.html create mode 100644 devtools/server/tests/mochitest/test_inspector-search.html create mode 100644 devtools/server/tests/mochitest/test_inspector-traversal.html create mode 100644 devtools/server/tests/mochitest/test_inspector_getImageData-wait-for-load.html create mode 100644 devtools/server/tests/mochitest/test_inspector_getImageData.html create mode 100644 devtools/server/tests/mochitest/test_inspector_getImageDataFromURL.html create mode 100644 devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html create mode 100644 devtools/server/tests/mochitest/test_makeGlobalObjectReference.html create mode 100644 devtools/server/tests/mochitest/test_memory.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_01.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_02.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_03.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_04.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_05.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_06.html create mode 100644 devtools/server/tests/mochitest/test_memory_allocations_07.html create mode 100644 devtools/server/tests/mochitest/test_memory_attach_01.html create mode 100644 devtools/server/tests/mochitest/test_memory_attach_02.html create mode 100644 devtools/server/tests/mochitest/test_memory_census.html create mode 100644 devtools/server/tests/mochitest/test_memory_gc_01.html create mode 100644 devtools/server/tests/mochitest/test_memory_gc_events.html create mode 100644 devtools/server/tests/mochitest/test_preference.html create mode 100644 devtools/server/tests/mochitest/test_settings.html create mode 100644 devtools/server/tests/mochitest/test_setupInParentChild.html create mode 100644 devtools/server/tests/mochitest/test_styles-applied.html create mode 100644 devtools/server/tests/mochitest/test_styles-computed.html create mode 100644 devtools/server/tests/mochitest/test_styles-layout.html create mode 100644 devtools/server/tests/mochitest/test_styles-matched.html create mode 100644 devtools/server/tests/mochitest/test_styles-modify.html create mode 100644 devtools/server/tests/mochitest/test_styles-svg.html create mode 100644 devtools/server/tests/mochitest/test_unsafeDereference.html create mode 100644 devtools/server/tests/mochitest/test_websocket-server.html create mode 100644 devtools/server/tests/unit/.eslintrc.js create mode 100644 devtools/server/tests/unit/addons/web-extension-upgrade/manifest.json create mode 100644 devtools/server/tests/unit/addons/web-extension/manifest.json create mode 100644 devtools/server/tests/unit/addons/web-extension2/manifest.json create mode 100644 devtools/server/tests/unit/babel_and_browserify_script_with_source_map.js create mode 100644 devtools/server/tests/unit/head_dbg.js create mode 100644 devtools/server/tests/unit/hello-actor.js create mode 100644 devtools/server/tests/unit/post_init_global_actors.js create mode 100644 devtools/server/tests/unit/post_init_tab_actors.js create mode 100644 devtools/server/tests/unit/pre_init_global_actors.js create mode 100644 devtools/server/tests/unit/pre_init_tab_actors.js create mode 100644 devtools/server/tests/unit/registertestactors-01.js create mode 100644 devtools/server/tests/unit/registertestactors-02.js create mode 100644 devtools/server/tests/unit/registertestactors-03.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-column-in-gcd-script.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-at-end-of-line.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-column.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-line-in-gcd-script.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-offsets.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-statements.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets.js create mode 100644 devtools/server/tests/unit/setBreakpoint-on-line.js create mode 100644 devtools/server/tests/unit/source-map-data/sourcemapped.coffee create mode 100644 devtools/server/tests/unit/source-map-data/sourcemapped.map create mode 100644 devtools/server/tests/unit/sourcemapped.js create mode 100644 devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_01.js create mode 100644 devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_02.js create mode 100644 devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_03.js create mode 100644 devtools/server/tests/unit/test_actor-registry-actor.js create mode 100644 devtools/server/tests/unit/test_add_actors.js create mode 100644 devtools/server/tests/unit/test_addon_reload.js create mode 100644 devtools/server/tests/unit/test_addons_actor.js create mode 100644 devtools/server/tests/unit/test_animation_name.js create mode 100644 devtools/server/tests/unit/test_animation_type.js create mode 100644 devtools/server/tests/unit/test_attach.js create mode 100644 devtools/server/tests/unit/test_blackboxing-01.js create mode 100644 devtools/server/tests/unit/test_blackboxing-02.js create mode 100644 devtools/server/tests/unit/test_blackboxing-03.js create mode 100644 devtools/server/tests/unit/test_blackboxing-04.js create mode 100644 devtools/server/tests/unit/test_blackboxing-05.js create mode 100644 devtools/server/tests/unit/test_blackboxing-06.js create mode 100644 devtools/server/tests/unit/test_blackboxing-07.js create mode 100644 devtools/server/tests/unit/test_breakpoint-01.js create mode 100644 devtools/server/tests/unit/test_breakpoint-02.js create mode 100644 devtools/server/tests/unit/test_breakpoint-03.js create mode 100644 devtools/server/tests/unit/test_breakpoint-04.js create mode 100644 devtools/server/tests/unit/test_breakpoint-05.js create mode 100644 devtools/server/tests/unit/test_breakpoint-06.js create mode 100644 devtools/server/tests/unit/test_breakpoint-07.js create mode 100644 devtools/server/tests/unit/test_breakpoint-08.js create mode 100644 devtools/server/tests/unit/test_breakpoint-09.js create mode 100644 devtools/server/tests/unit/test_breakpoint-10.js create mode 100644 devtools/server/tests/unit/test_breakpoint-11.js create mode 100644 devtools/server/tests/unit/test_breakpoint-12.js create mode 100644 devtools/server/tests/unit/test_breakpoint-13.js create mode 100644 devtools/server/tests/unit/test_breakpoint-14.js create mode 100644 devtools/server/tests/unit/test_breakpoint-15.js create mode 100644 devtools/server/tests/unit/test_breakpoint-16.js create mode 100644 devtools/server/tests/unit/test_breakpoint-17.js create mode 100644 devtools/server/tests/unit/test_breakpoint-18.js create mode 100644 devtools/server/tests/unit/test_breakpoint-19.js create mode 100644 devtools/server/tests/unit/test_breakpoint-20.js create mode 100644 devtools/server/tests/unit/test_breakpoint-21.js create mode 100644 devtools/server/tests/unit/test_breakpoint-actor-map.js create mode 100644 devtools/server/tests/unit/test_client_close.js create mode 100644 devtools/server/tests/unit/test_client_request.js create mode 100644 devtools/server/tests/unit/test_conditional_breakpoint-01.js create mode 100644 devtools/server/tests/unit/test_conditional_breakpoint-02.js create mode 100644 devtools/server/tests/unit/test_conditional_breakpoint-03.js create mode 100644 devtools/server/tests/unit/test_dbgactor.js create mode 100644 devtools/server/tests/unit/test_dbgclient_debuggerstatement.js create mode 100644 devtools/server/tests/unit/test_dbgglobal.js create mode 100644 devtools/server/tests/unit/test_eval-01.js create mode 100644 devtools/server/tests/unit/test_eval-02.js create mode 100644 devtools/server/tests/unit/test_eval-03.js create mode 100644 devtools/server/tests/unit/test_eval-04.js create mode 100644 devtools/server/tests/unit/test_eval-05.js create mode 100644 devtools/server/tests/unit/test_eventlooplag_actor.js create mode 100644 devtools/server/tests/unit/test_forwardingprefix.js create mode 100644 devtools/server/tests/unit/test_frameactor-01.js create mode 100644 devtools/server/tests/unit/test_frameactor-02.js create mode 100644 devtools/server/tests/unit/test_frameactor-03.js create mode 100644 devtools/server/tests/unit/test_frameactor-04.js create mode 100644 devtools/server/tests/unit/test_frameactor-05.js create mode 100644 devtools/server/tests/unit/test_framearguments-01.js create mode 100644 devtools/server/tests/unit/test_framebindings-01.js create mode 100644 devtools/server/tests/unit/test_framebindings-02.js create mode 100644 devtools/server/tests/unit/test_framebindings-03.js create mode 100644 devtools/server/tests/unit/test_framebindings-04.js create mode 100644 devtools/server/tests/unit/test_framebindings-05.js create mode 100644 devtools/server/tests/unit/test_framebindings-06.js create mode 100644 devtools/server/tests/unit/test_framebindings-07.js create mode 100644 devtools/server/tests/unit/test_frameclient-01.js create mode 100644 devtools/server/tests/unit/test_frameclient-02.js create mode 100644 devtools/server/tests/unit/test_functiongrips-01.js create mode 100644 devtools/server/tests/unit/test_get-executable-lines-source-map.js create mode 100644 devtools/server/tests/unit/test_get-executable-lines.js create mode 100644 devtools/server/tests/unit/test_getRuleText.js create mode 100644 devtools/server/tests/unit/test_getTextAtLineColumn.js create mode 100644 devtools/server/tests/unit/test_getyoungestframe.js create mode 100644 devtools/server/tests/unit/test_ignore_caught_exceptions.js create mode 100644 devtools/server/tests/unit/test_ignore_no_interface_exceptions.js create mode 100644 devtools/server/tests/unit/test_interrupt.js create mode 100644 devtools/server/tests/unit/test_layout-reflows-observer.js create mode 100644 devtools/server/tests/unit/test_listsources-01.js create mode 100644 devtools/server/tests/unit/test_listsources-02.js create mode 100644 devtools/server/tests/unit/test_listsources-03.js create mode 100644 devtools/server/tests/unit/test_listsources-04.js create mode 100644 devtools/server/tests/unit/test_longstringactor.js create mode 100644 devtools/server/tests/unit/test_longstringgrips-01.js create mode 100644 devtools/server/tests/unit/test_longstringgrips-02.js create mode 100644 devtools/server/tests/unit/test_monitor_actor.js create mode 100644 devtools/server/tests/unit/test_nativewrappers.js create mode 100644 devtools/server/tests/unit/test_nesting-01.js create mode 100644 devtools/server/tests/unit/test_nesting-02.js create mode 100644 devtools/server/tests/unit/test_nesting-03.js create mode 100644 devtools/server/tests/unit/test_new_source-01.js create mode 100644 devtools/server/tests/unit/test_nodelistactor.js create mode 100644 devtools/server/tests/unit/test_nsjsinspector.js create mode 100644 devtools/server/tests/unit/test_objectgrips-01.js create mode 100644 devtools/server/tests/unit/test_objectgrips-02.js create mode 100644 devtools/server/tests/unit/test_objectgrips-03.js create mode 100644 devtools/server/tests/unit/test_objectgrips-04.js create mode 100644 devtools/server/tests/unit/test_objectgrips-05.js create mode 100644 devtools/server/tests/unit/test_objectgrips-06.js create mode 100644 devtools/server/tests/unit/test_objectgrips-07.js create mode 100644 devtools/server/tests/unit/test_objectgrips-08.js create mode 100644 devtools/server/tests/unit/test_objectgrips-09.js create mode 100644 devtools/server/tests/unit/test_objectgrips-10.js create mode 100644 devtools/server/tests/unit/test_objectgrips-11.js create mode 100644 devtools/server/tests/unit/test_objectgrips-12.js create mode 100644 devtools/server/tests/unit/test_objectgrips-13.js create mode 100644 devtools/server/tests/unit/test_pause_exceptions-01.js create mode 100644 devtools/server/tests/unit/test_pause_exceptions-02.js create mode 100644 devtools/server/tests/unit/test_pauselifetime-01.js create mode 100644 devtools/server/tests/unit/test_pauselifetime-02.js create mode 100644 devtools/server/tests/unit/test_pauselifetime-03.js create mode 100644 devtools/server/tests/unit/test_pauselifetime-04.js create mode 100644 devtools/server/tests/unit/test_profiler_activation-01.js create mode 100644 devtools/server/tests/unit/test_profiler_activation-02.js create mode 100644 devtools/server/tests/unit/test_profiler_bufferstatus.js create mode 100644 devtools/server/tests/unit/test_profiler_close.js create mode 100644 devtools/server/tests/unit/test_profiler_data.js create mode 100644 devtools/server/tests/unit/test_profiler_events-01.js create mode 100644 devtools/server/tests/unit/test_profiler_events-02.js create mode 100644 devtools/server/tests/unit/test_profiler_getbufferinfo.js create mode 100644 devtools/server/tests/unit/test_profiler_getfeatures.js create mode 100644 devtools/server/tests/unit/test_profiler_getsharedlibraryinformation.js create mode 100644 devtools/server/tests/unit/test_promise_state-01.js create mode 100644 devtools/server/tests/unit/test_promise_state-02.js create mode 100644 devtools/server/tests/unit/test_promise_state-03.js create mode 100644 devtools/server/tests/unit/test_promises_actor_attach.js create mode 100644 devtools/server/tests/unit/test_promises_actor_exist.js create mode 100644 devtools/server/tests/unit/test_promises_actor_list_promises.js create mode 100644 devtools/server/tests/unit/test_promises_actor_onnewpromise.js create mode 100644 devtools/server/tests/unit/test_promises_actor_onpromisesettled.js create mode 100644 devtools/server/tests/unit/test_promises_client_getdependentpromises.js create mode 100644 devtools/server/tests/unit/test_promises_object_creationtimestamp.js create mode 100644 devtools/server/tests/unit/test_promises_object_timetosettle-01.js create mode 100644 devtools/server/tests/unit/test_promises_object_timetosettle-02.js create mode 100644 devtools/server/tests/unit/test_protocolSpec.js create mode 100644 devtools/server/tests/unit/test_protocol_abort.js create mode 100644 devtools/server/tests/unit/test_protocol_async.js create mode 100644 devtools/server/tests/unit/test_protocol_children.js create mode 100644 devtools/server/tests/unit/test_protocol_formtype.js create mode 100644 devtools/server/tests/unit/test_protocol_longstring.js create mode 100644 devtools/server/tests/unit/test_protocol_simple.js create mode 100644 devtools/server/tests/unit/test_protocol_stack.js create mode 100644 devtools/server/tests/unit/test_protocol_unregister.js create mode 100644 devtools/server/tests/unit/test_reattach-thread.js create mode 100644 devtools/server/tests/unit/test_registerClient.js create mode 100644 devtools/server/tests/unit/test_register_actor.js create mode 100644 devtools/server/tests/unit/test_requestTypes.js create mode 100644 devtools/server/tests/unit/test_safe-getter.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-column-in-gcd-script.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-column-with-no-offsets-at-end-of-line.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-column.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-line-in-gcd-script.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-offsets.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-statements.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets.js create mode 100644 devtools/server/tests/unit/test_setBreakpoint-on-line.js create mode 100644 devtools/server/tests/unit/test_source-01.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-01.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-02.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-03.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-04.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-05.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-06.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-07.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-08.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-09.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-10.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-11.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-12.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-13.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-16.js create mode 100644 devtools/server/tests/unit/test_sourcemaps-17.js create mode 100644 devtools/server/tests/unit/test_stepping-01.js create mode 100644 devtools/server/tests/unit/test_stepping-02.js create mode 100644 devtools/server/tests/unit/test_stepping-03.js create mode 100644 devtools/server/tests/unit/test_stepping-04.js create mode 100644 devtools/server/tests/unit/test_stepping-05.js create mode 100644 devtools/server/tests/unit/test_stepping-06.js create mode 100644 devtools/server/tests/unit/test_stepping-07.js create mode 100644 devtools/server/tests/unit/test_symbols-01.js create mode 100644 devtools/server/tests/unit/test_symbols-02.js create mode 100644 devtools/server/tests/unit/test_threadlifetime-01.js create mode 100644 devtools/server/tests/unit/test_threadlifetime-02.js create mode 100644 devtools/server/tests/unit/test_threadlifetime-03.js create mode 100644 devtools/server/tests/unit/test_threadlifetime-04.js create mode 100644 devtools/server/tests/unit/test_threadlifetime-05.js create mode 100644 devtools/server/tests/unit/test_threadlifetime-06.js create mode 100644 devtools/server/tests/unit/test_unsafeDereference.js create mode 100644 devtools/server/tests/unit/test_xpcshell_debugging.js create mode 100644 devtools/server/tests/unit/testactors.js create mode 100644 devtools/server/tests/unit/tracerlocations.js create mode 100644 devtools/server/tests/unit/xpcshell.ini create mode 100644 devtools/server/tests/unit/xpcshell_debugging_script.js create mode 100644 devtools/server/websocket-server.js create mode 100644 devtools/server/worker.js (limited to 'devtools/server') diff --git a/devtools/server/actors/actor-registry.js b/devtools/server/actors/actor-registry.js new file mode 100644 index 000000000..6a083ba6f --- /dev/null +++ b/devtools/server/actors/actor-registry.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const protocol = require("devtools/shared/protocol"); +const { method, custom, Arg, Option, RetVal } = protocol; + +const { Cu, CC, components } = require("chrome"); +const Services = require("Services"); +const { DebuggerServer } = require("devtools/server/main"); +const { registerActor, unregisterActor } = require("devtools/server/actors/utils/actor-registry-utils"); +const { actorActorSpec, actorRegistrySpec } = require("devtools/shared/specs/actor-registry"); + +/** + * The ActorActor gives you a handle to an actor you've dynamically + * registered and allows you to unregister it. + */ +const ActorActor = protocol.ActorClassWithSpec(actorActorSpec, { + initialize: function (conn, options) { + protocol.Actor.prototype.initialize.call(this, conn); + + this.options = options; + }, + + unregister: function () { + unregisterActor(this.options); + } +}); + +/* + * The ActorRegistryActor allows clients to define new actors on the + * server. This is particularly useful for addons. + */ +const ActorRegistryActor = protocol.ActorClassWithSpec(actorRegistrySpec, { + initialize: function (conn) { + protocol.Actor.prototype.initialize.call(this, conn); + }, + + registerActor: function (sourceText, fileName, options) { + return registerActor(sourceText, fileName, options).then(() => { + let { constructor, type } = options; + + return ActorActor(this.conn, { + name: constructor, + tab: type.tab, + global: type.global + }); + }); + } +}); + +exports.ActorRegistryActor = ActorRegistryActor; diff --git a/devtools/server/actors/addon.js b/devtools/server/actors/addon.js new file mode 100644 index 000000000..7f152e984 --- /dev/null +++ b/devtools/server/actors/addon.js @@ -0,0 +1,352 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { Ci, Cu } = require("chrome"); +var Services = require("Services"); +var { ActorPool } = require("devtools/server/actors/common"); +var { TabSources } = require("./utils/TabSources"); +var makeDebugger = require("./utils/make-debugger"); +var { ConsoleAPIListener } = require("devtools/server/actors/utils/webconsole-utils"); +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var { assert, update } = DevToolsUtils; + +loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); +loader.lazyRequireGetter(this, "WebConsoleActor", "devtools/server/actors/webconsole", true); + +loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); + +function BrowserAddonActor(aConnection, aAddon) { + this.conn = aConnection; + this._addon = aAddon; + this._contextPool = new ActorPool(this.conn); + this.conn.addActorPool(this._contextPool); + this.threadActor = null; + this._global = null; + + this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this); + + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: this._findDebuggees.bind(this), + shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee + }); + + AddonManager.addAddonListener(this); +} +exports.BrowserAddonActor = BrowserAddonActor; + +BrowserAddonActor.prototype = { + actorPrefix: "addon", + + get exited() { + return !this._addon; + }, + + get id() { + return this._addon.id; + }, + + get url() { + return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined; + }, + + get attached() { + return this.threadActor; + }, + + get global() { + return this._global; + }, + + get sources() { + if (!this._sources) { + assert(this.threadActor, "threadActor should exist when creating sources."); + this._sources = new TabSources(this.threadActor, this._allowSource); + } + return this._sources; + }, + + + form: function BAA_form() { + assert(this.actorID, "addon should have an actorID."); + if (!this._consoleActor) { + this._consoleActor = new AddonConsoleActor(this._addon, this.conn, this); + this._contextPool.addActor(this._consoleActor); + } + + return { + actor: this.actorID, + id: this.id, + name: this._addon.name, + url: this.url, + iconURL: this._addon.iconURL, + debuggable: this._addon.isDebuggable, + temporarilyInstalled: this._addon.temporarilyInstalled, + consoleActor: this._consoleActor.actorID, + + traits: { + highlightable: false, + networkMonitor: false, + }, + }; + }, + + disconnect: function BAA_disconnect() { + this.conn.removeActorPool(this._contextPool); + this._contextPool = null; + this._consoleActor = null; + this._addon = null; + this._global = null; + AddonManager.removeAddonListener(this); + }, + + setOptions: function BAA_setOptions(aOptions) { + if ("global" in aOptions) { + this._global = aOptions.global; + } + }, + + onInstalled: function BAA_updateAddonWrapper(aAddon) { + if (aAddon.id != this._addon.id) { + return; + } + + // Update the AddonManager's addon object on reload/update. + this._addon = aAddon; + }, + + onDisabled: function BAA_onDisabled(aAddon) { + if (aAddon != this._addon) { + return; + } + + this._global = null; + }, + + onUninstalled: function BAA_onUninstalled(aAddon) { + if (aAddon != this._addon) { + return; + } + + if (this.attached) { + this.onDetach(); + + // The BrowserAddonActor is not a TabActor and it has to send + // "tabDetached" directly to close the devtools toolbox window. + this.conn.send({ from: this.actorID, type: "tabDetached" }); + } + + this.disconnect(); + }, + + onAttach: function BAA_onAttach() { + if (this.exited) { + return { type: "exited" }; + } + + if (!this.attached) { + this.threadActor = new AddonThreadActor(this.conn, this); + this._contextPool.addActor(this.threadActor); + } + + return { type: "tabAttached", threadActor: this.threadActor.actorID }; + }, + + onDetach: function BAA_onDetach() { + if (!this.attached) { + return { error: "wrongState" }; + } + + this._contextPool.removeActor(this.threadActor); + + this.threadActor = null; + this._sources = null; + + return { type: "detached" }; + }, + + onReload: function BAA_onReload() { + return this._addon.reload() + .then(() => { + return {}; // send an empty response + }); + }, + + preNest: function () { + let e = Services.wm.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.suppressEventHandling(true); + windowUtils.suspendTimeouts(); + } + }, + + postNest: function () { + let e = Services.wm.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.resumeTimeouts(); + windowUtils.suppressEventHandling(false); + } + }, + + /** + * Return true if the given global is associated with this addon and should be + * added as a debuggee, false otherwise. + */ + _shouldAddNewGlobalAsDebuggee: function (aGlobal) { + const global = unwrapDebuggerObjectGlobal(aGlobal); + try { + // This will fail for non-Sandbox objects, hence the try-catch block. + let metadata = Cu.getSandboxMetadata(global); + if (metadata) { + return metadata.addonID === this.id; + } + } catch (e) {} + + if (global instanceof Ci.nsIDOMWindow) { + return mapURIToAddonID(global.document.documentURIObject) == this.id; + } + + // Check the global for a __URI__ property and then try to map that to an + // add-on + let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__"); + if (uridescriptor && "value" in uridescriptor && uridescriptor.value) { + let uri; + try { + uri = Services.io.newURI(uridescriptor.value, null, null); + } + catch (e) { + DevToolsUtils.reportException( + "BrowserAddonActor.prototype._shouldAddNewGlobalAsDebuggee", + new Error("Invalid URI: " + uridescriptor.value) + ); + return false; + } + + if (mapURIToAddonID(uri) == this.id) { + return true; + } + } + + return false; + }, + + /** + * Override the eligibility check for scripts and sources to make + * sure every script and source with a URL is stored when debugging + * add-ons. + */ + _allowSource: function (aSource) { + // XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it. + if (aSource.url === "resource://gre/modules/addons/XPIProvider.jsm") { + return false; + } + + return true; + }, + + /** + * Yield the current set of globals associated with this addon that should be + * added as debuggees. + */ + _findDebuggees: function (dbg) { + return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee); + } +}; + +BrowserAddonActor.prototype.requestTypes = { + "attach": BrowserAddonActor.prototype.onAttach, + "detach": BrowserAddonActor.prototype.onDetach, + "reload": BrowserAddonActor.prototype.onReload +}; + +/** + * The AddonConsoleActor implements capabilities needed for the add-on web + * console feature. + * + * @constructor + * @param object aAddon + * The add-on that this console watches. + * @param object aConnection + * The connection to the client, DebuggerServerConnection. + * @param object aParentActor + * The parent BrowserAddonActor actor. + */ +function AddonConsoleActor(aAddon, aConnection, aParentActor) +{ + this.addon = aAddon; + WebConsoleActor.call(this, aConnection, aParentActor); +} + +AddonConsoleActor.prototype = Object.create(WebConsoleActor.prototype); + +update(AddonConsoleActor.prototype, { + constructor: AddonConsoleActor, + + actorPrefix: "addonConsole", + + /** + * The add-on that this console watches. + */ + addon: null, + + /** + * The main add-on JS global + */ + get window() { + return this.parentActor.global; + }, + + /** + * Destroy the current AddonConsoleActor instance. + */ + disconnect: function ACA_disconnect() + { + WebConsoleActor.prototype.disconnect.call(this); + this.addon = null; + }, + + /** + * Handler for the "startListeners" request. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response object which holds the startedListeners array. + */ + onStartListeners: function ACA_onStartListeners(aRequest) + { + let startedListeners = []; + + while (aRequest.listeners.length > 0) { + let listener = aRequest.listeners.shift(); + switch (listener) { + case "ConsoleAPI": + if (!this.consoleAPIListener) { + this.consoleAPIListener = + new ConsoleAPIListener(null, this, { addonId: this.addon.id }); + this.consoleAPIListener.init(); + } + startedListeners.push(listener); + break; + } + } + return { + startedListeners: startedListeners, + nativeConsoleAPI: true, + traits: this.traits, + }; + }, +}); + +AddonConsoleActor.prototype.requestTypes = Object.create(WebConsoleActor.prototype.requestTypes); +AddonConsoleActor.prototype.requestTypes.startListeners = AddonConsoleActor.prototype.onStartListeners; diff --git a/devtools/server/actors/addons.js b/devtools/server/actors/addons.js new file mode 100644 index 000000000..297a3a438 --- /dev/null +++ b/devtools/server/actors/addons.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {AddonManager} = require("resource://gre/modules/AddonManager.jsm"); +const protocol = require("devtools/shared/protocol"); +const {FileUtils} = require("resource://gre/modules/FileUtils.jsm"); +const {Task} = require("devtools/shared/task"); +const {addonsSpec} = require("devtools/shared/specs/addons"); + +const AddonsActor = protocol.ActorClassWithSpec(addonsSpec, { + + initialize: function (conn) { + protocol.Actor.prototype.initialize.call(this, conn); + }, + + installTemporaryAddon: Task.async(function* (addonPath) { + let addonFile; + let addon; + try { + addonFile = new FileUtils.File(addonPath); + addon = yield AddonManager.installTemporaryAddon(addonFile); + } catch (error) { + throw new Error(`Could not install add-on at '${addonPath}': ${error}`); + } + + // TODO: once the add-on actor has been refactored to use + // protocol.js, we could return it directly. + // return new BrowserAddonActor(this.conn, addon); + + // Return a pseudo add-on object that a calling client can work + // with. Provide a flag that the client can use to detect when it + // gets upgraded to a real actor object. + return { id: addon.id, actor: false }; + + }), +}); + +exports.AddonsActor = AddonsActor; diff --git a/devtools/server/actors/animation.js b/devtools/server/actors/animation.js new file mode 100644 index 000000000..642c4bcaf --- /dev/null +++ b/devtools/server/actors/animation.js @@ -0,0 +1,751 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Set of actors that expose the Web Animations API to devtools protocol + * clients. + * + * The |Animations| actor is the main entry point. It is used to discover + * animation players on given nodes. + * There should only be one instance per debugger server. + * + * The |AnimationPlayer| actor provides attributes and methods to inspect an + * animation as well as pause/resume/seek it. + * + * The Web Animation spec implementation is ongoing in Gecko, and so this set + * of actors should evolve when the implementation progresses. + * + * References: + * - WebAnimation spec: + * http://w3c.github.io/web-animations/ + * - WebAnimation WebIDL files: + * /dom/webidl/Animation*.webidl + */ + +const {Cu} = require("chrome"); +const promise = require("promise"); +const {Task} = require("devtools/shared/task"); +const protocol = require("devtools/shared/protocol"); +const {Actor, ActorClassWithSpec} = protocol; +const {animationPlayerSpec, animationsSpec} = require("devtools/shared/specs/animation"); +const events = require("sdk/event/core"); + +// Types of animations. +const ANIMATION_TYPES = { + CSS_ANIMATION: "cssanimation", + CSS_TRANSITION: "csstransition", + SCRIPT_ANIMATION: "scriptanimation", + UNKNOWN: "unknown" +}; +exports.ANIMATION_TYPES = ANIMATION_TYPES; + +/** + * The AnimationPlayerActor provides information about a given animation: its + * startTime, currentTime, current state, etc. + * + * Since the state of a player changes as the animation progresses it is often + * useful to call getCurrentState at regular intervals to get the current state. + * + * This actor also allows playing, pausing and seeking the animation. + */ +var AnimationPlayerActor = protocol.ActorClassWithSpec(animationPlayerSpec, { + /** + * @param {AnimationsActor} The main AnimationsActor instance + * @param {AnimationPlayer} The player object returned by getAnimationPlayers + */ + initialize: function (animationsActor, player) { + Actor.prototype.initialize.call(this, animationsActor.conn); + + this.onAnimationMutation = this.onAnimationMutation.bind(this); + + this.walker = animationsActor.walker; + this.player = player; + + // Listen to animation mutations on the node to alert the front when the + // current animation changes. + // If the node is a pseudo-element, then we listen on its parent with + // subtree:true (there's no risk of getting too many notifications in + // onAnimationMutation since we filter out events that aren't for the + // current animation). + this.observer = new this.window.MutationObserver(this.onAnimationMutation); + if (this.isPseudoElement) { + this.observer.observe(this.node.parentElement, + {animations: true, subtree: true}); + } else { + this.observer.observe(this.node, {animations: true}); + } + }, + + destroy: function () { + // Only try to disconnect the observer if it's not already dead (i.e. if the + // container view hasn't navigated since). + if (this.observer && !Cu.isDeadWrapper(this.observer)) { + this.observer.disconnect(); + } + this.player = this.observer = this.walker = null; + + Actor.prototype.destroy.call(this); + }, + + get isPseudoElement() { + return !this.player.effect.target.ownerDocument; + }, + + get node() { + if (this._node) { + return this._node; + } + + let node = this.player.effect.target; + + if (this.isPseudoElement) { + // The target is a CSSPseudoElement object which just has a property that + // points to its parent element and a string type (::before or ::after). + let treeWalker = this.walker.getDocumentWalker(node.parentElement); + while (treeWalker.nextNode()) { + let currentNode = treeWalker.currentNode; + if ((currentNode.nodeName === "_moz_generated_content_before" && + node.type === "::before") || + (currentNode.nodeName === "_moz_generated_content_after" && + node.type === "::after")) { + this._node = currentNode; + } + } + } else { + // The target is a DOM node. + this._node = node; + } + + return this._node; + }, + + get window() { + return this.node.ownerDocument.defaultView; + }, + + /** + * Release the actor, when it isn't needed anymore. + * Protocol.js uses this release method to call the destroy method. + */ + release: function () {}, + + form: function (detail) { + if (detail === "actorid") { + return this.actorID; + } + + let data = this.getCurrentState(); + data.actor = this.actorID; + + // If we know the WalkerActor, and if the animated node is known by it, then + // return its corresponding NodeActor ID too. + if (this.walker && this.walker.hasNode(this.node)) { + data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID; + } + + return data; + }, + + isCssAnimation: function (player = this.player) { + return player instanceof this.window.CSSAnimation; + }, + + isCssTransition: function (player = this.player) { + return player instanceof this.window.CSSTransition; + }, + + isScriptAnimation: function (player = this.player) { + return player instanceof this.window.Animation && !( + player instanceof this.window.CSSAnimation || + player instanceof this.window.CSSTransition + ); + }, + + getType: function () { + if (this.isCssAnimation()) { + return ANIMATION_TYPES.CSS_ANIMATION; + } else if (this.isCssTransition()) { + return ANIMATION_TYPES.CSS_TRANSITION; + } else if (this.isScriptAnimation()) { + return ANIMATION_TYPES.SCRIPT_ANIMATION; + } + + return ANIMATION_TYPES.UNKNOWN; + }, + + /** + * Get the name of this animation. This can be either the animation.id + * property if it was set, or the keyframe rule name or the transition + * property. + * @return {String} + */ + getName: function () { + if (this.player.id) { + return this.player.id; + } else if (this.isCssAnimation()) { + return this.player.animationName; + } else if (this.isCssTransition()) { + return this.player.transitionProperty; + } + + return ""; + }, + + /** + * Get the animation duration from this player, in milliseconds. + * @return {Number} + */ + getDuration: function () { + return this.player.effect.getComputedTiming().duration; + }, + + /** + * Get the animation delay from this player, in milliseconds. + * @return {Number} + */ + getDelay: function () { + return this.player.effect.getComputedTiming().delay; + }, + + /** + * Get the animation endDelay from this player, in milliseconds. + * @return {Number} + */ + getEndDelay: function () { + return this.player.effect.getComputedTiming().endDelay; + }, + + /** + * Get the animation iteration count for this player. That is, how many times + * is the animation scheduled to run. + * @return {Number} The number of iterations, or null if the animation repeats + * infinitely. + */ + getIterationCount: function () { + let iterations = this.player.effect.getComputedTiming().iterations; + return iterations === "Infinity" ? null : iterations; + }, + + /** + * Get the animation iterationStart from this player, in ratio. + * That is offset of starting position of the animation. + * @return {Number} + */ + getIterationStart: function () { + return this.player.effect.getComputedTiming().iterationStart; + }, + + /** + * Get the animation easing from this player. + * @return {String} + */ + getEasing: function () { + return this.player.effect.timing.easing; + }, + + /** + * Get the animation fill mode from this player. + * @return {String} + */ + getFill: function () { + return this.player.effect.getComputedTiming().fill; + }, + + /** + * Get the animation direction from this player. + * @return {String} + */ + getDirection: function () { + return this.player.effect.getComputedTiming().direction; + }, + + getPropertiesCompositorStatus: function () { + let properties = this.player.effect.getProperties(); + return properties.map(prop => { + return { + property: prop.property, + runningOnCompositor: prop.runningOnCompositor, + warning: prop.warning + }; + }); + }, + + /** + * Return the current start of the Animation. + * @return {Object} + */ + getState: function () { + // Remember the startTime each time getState is called, it may be useful + // when animations get paused. As in, when an animation gets paused, its + // startTime goes back to null, but the front-end might still be interested + // in knowing what the previous startTime was. So everytime it is set, + // remember it and send it along with the newState. + if (this.player.startTime) { + this.previousStartTime = this.player.startTime; + } + + // Note that if you add a new property to the state object, make sure you + // add the corresponding property in the AnimationPlayerFront' initialState + // getter. + return { + type: this.getType(), + // startTime is null whenever the animation is paused or waiting to start. + startTime: this.player.startTime, + previousStartTime: this.previousStartTime, + currentTime: this.player.currentTime, + playState: this.player.playState, + playbackRate: this.player.playbackRate, + name: this.getName(), + duration: this.getDuration(), + delay: this.getDelay(), + endDelay: this.getEndDelay(), + iterationCount: this.getIterationCount(), + iterationStart: this.getIterationStart(), + fill: this.getFill(), + easing: this.getEasing(), + direction: this.getDirection(), + // animation is hitting the fast path or not. Returns false whenever the + // animation is paused as it is taken off the compositor then. + isRunningOnCompositor: + this.getPropertiesCompositorStatus() + .some(propState => propState.runningOnCompositor), + propertyState: this.getPropertiesCompositorStatus(), + // The document timeline's currentTime is being sent along too. This is + // not strictly related to the node's animationPlayer, but is useful to + // know the current time of the animation with respect to the document's. + documentCurrentTime: this.node.ownerDocument.timeline.currentTime + }; + }, + + /** + * Get the current state of the AnimationPlayer (currentTime, playState, ...). + * Note that the initial state is returned as the form of this actor when it + * is initialized. + * This protocol method only returns a trimed down version of this state in + * case some properties haven't changed since last time (since the front can + * reconstruct those). If you want the full state, use the getState method. + * @return {Object} + */ + getCurrentState: function () { + let newState = this.getState(); + + // If we've saved a state before, compare and only send what has changed. + // It's expected of the front to also save old states to re-construct the + // full state when an incomplete one is received. + // This is to minimize protocol traffic. + let sentState = {}; + if (this.currentState) { + for (let key in newState) { + if (typeof this.currentState[key] === "undefined" || + this.currentState[key] !== newState[key]) { + sentState[key] = newState[key]; + } + } + } else { + sentState = newState; + } + this.currentState = newState; + + return sentState; + }, + + /** + * Executed when the current animation changes, used to emit the new state + * the the front. + */ + onAnimationMutation: function (mutations) { + let isCurrentAnimation = animation => animation === this.player; + let hasCurrentAnimation = animations => animations.some(isCurrentAnimation); + let hasChanged = false; + + for (let {removedAnimations, changedAnimations} of mutations) { + if (hasCurrentAnimation(removedAnimations)) { + // Reset the local copy of the state on removal, since the animation can + // be kept on the client and re-added, its state needs to be sent in + // full. + this.currentState = null; + } + + if (hasCurrentAnimation(changedAnimations)) { + // Only consider the state has having changed if any of delay, duration, + // iterationcount or iterationStart has changed (for now at least). + let newState = this.getState(); + let oldState = this.currentState; + hasChanged = newState.delay !== oldState.delay || + newState.iterationCount !== oldState.iterationCount || + newState.iterationStart !== oldState.iterationStart || + newState.duration !== oldState.duration || + newState.endDelay !== oldState.endDelay; + break; + } + } + + if (hasChanged) { + events.emit(this, "changed", this.getCurrentState()); + } + }, + + /** + * Pause the player. + */ + pause: function () { + this.player.pause(); + return this.player.ready; + }, + + /** + * Play the player. + * This method only returns when the animation has left its pending state. + */ + play: function () { + this.player.play(); + return this.player.ready; + }, + + /** + * Simply exposes the player ready promise. + * + * When an animation is created/paused then played, there's a short time + * during which its playState is pending, before being set to running. + * + * If you either created a new animation using the Web Animations API or + * paused/played an existing one, and then want to access the playState, you + * might be interested to call this method. + * This is especially important for tests. + */ + ready: function () { + return this.player.ready; + }, + + /** + * Set the current time of the animation player. + */ + setCurrentTime: function (currentTime) { + // The spec is that the progress of animation is changed + // if the time of setCurrentTime is during the endDelay. + // We should prevent the time + // to make the same animation behavior as the original. + // Likewise, in case the time is less than 0. + const timing = this.player.effect.getComputedTiming(); + if (timing.delay < 0) { + currentTime += timing.delay; + } + if (currentTime < 0) { + currentTime = 0; + } else if (currentTime * this.player.playbackRate > timing.endTime) { + currentTime = timing.endTime; + } + this.player.currentTime = currentTime * this.player.playbackRate; + }, + + /** + * Set the playback rate of the animation player. + */ + setPlaybackRate: function (playbackRate) { + this.player.playbackRate = playbackRate; + }, + + /** + * Get data about the keyframes of this animation player. + * @return {Object} Returns a list of frames, each frame containing the list + * animated properties as well as the frame's offset. + */ + getFrames: function () { + return this.player.effect.getKeyframes(); + }, + + /** + * Get data about the animated properties of this animation player. + * @return {Array} Returns a list of animated properties. + * Each property contains a list of values and their offsets + */ + getProperties: function () { + return this.player.effect.getProperties().map(property => { + return {name: property.property, values: property.values}; + }); + } +}); + +exports.AnimationPlayerActor = AnimationPlayerActor; + +/** + * The Animations actor lists animation players for a given node. + */ +var AnimationsActor = exports.AnimationsActor = protocol.ActorClassWithSpec(animationsSpec, { + initialize: function(conn, tabActor) { + Actor.prototype.initialize.call(this, conn); + this.tabActor = tabActor; + + this.onWillNavigate = this.onWillNavigate.bind(this); + this.onNavigate = this.onNavigate.bind(this); + this.onAnimationMutation = this.onAnimationMutation.bind(this); + + this.allAnimationsPaused = false; + events.on(this.tabActor, "will-navigate", this.onWillNavigate); + events.on(this.tabActor, "navigate", this.onNavigate); + }, + + destroy: function () { + Actor.prototype.destroy.call(this); + events.off(this.tabActor, "will-navigate", this.onWillNavigate); + events.off(this.tabActor, "navigate", this.onNavigate); + + this.stopAnimationPlayerUpdates(); + this.tabActor = this.observer = this.actors = this.walker = null; + }, + + /** + * Since AnimationsActor doesn't have a protocol.js parent actor that takes + * care of its lifetime, implementing disconnect is required to cleanup. + */ + disconnect: function () { + this.destroy(); + }, + + /** + * Clients can optionally call this with a reference to their WalkerActor. + * If they do, then AnimationPlayerActor's forms are going to also include + * NodeActor IDs when the corresponding NodeActors do exist. + * This, in turns, is helpful for clients to avoid having to go back once more + * to the server to get a NodeActor for a particular animation. + * @param {WalkerActor} walker + */ + setWalkerActor: function (walker) { + this.walker = walker; + }, + + /** + * Retrieve the list of AnimationPlayerActor actors for currently running + * animations on a node and its descendants. + * Note that calling this method a second time will destroy all previously + * retrieved AnimationPlayerActors. Indeed, the lifecycle of these actors + * is managed here on the server and tied to getAnimationPlayersForNode + * being called. + * @param {NodeActor} nodeActor The NodeActor as defined in + * /devtools/server/actors/inspector + */ + getAnimationPlayersForNode: function (nodeActor) { + let animations = nodeActor.rawNode.getAnimations({subtree: true}); + + // Destroy previously stored actors + if (this.actors) { + this.actors.forEach(actor => actor.destroy()); + } + this.actors = []; + + for (let i = 0; i < animations.length; i++) { + let actor = AnimationPlayerActor(this, animations[i]); + this.actors.push(actor); + } + + // When a front requests the list of players for a node, start listening + // for animation mutations on this node to send updates to the front, until + // either getAnimationPlayersForNode is called again or + // stopAnimationPlayerUpdates is called. + this.stopAnimationPlayerUpdates(); + let win = nodeActor.rawNode.ownerDocument.defaultView; + this.observer = new win.MutationObserver(this.onAnimationMutation); + this.observer.observe(nodeActor.rawNode, { + animations: true, + subtree: true + }); + + return this.actors; + }, + + onAnimationMutation: function (mutations) { + let eventData = []; + let readyPromises = []; + + for (let {addedAnimations, removedAnimations} of mutations) { + for (let player of removedAnimations) { + // Note that animations are reported as removed either when they are + // actually removed from the node (e.g. css class removed) or when they + // are finished and don't have forwards animation-fill-mode. + // In the latter case, we don't send an event, because the corresponding + // animation can still be seeked/resumed, so we want the client to keep + // its reference to the AnimationPlayerActor. + if (player.playState !== "idle") { + continue; + } + + let index = this.actors.findIndex(a => a.player === player); + if (index !== -1) { + eventData.push({ + type: "removed", + player: this.actors[index] + }); + this.actors.splice(index, 1); + } + } + + for (let player of addedAnimations) { + // If the added player already exists, it means we previously filtered + // it out when it was reported as removed. So filter it out here too. + if (this.actors.find(a => a.player === player)) { + continue; + } + + // If the added player has the same name and target node as a player we + // already have, it means it's a transition that's re-starting. So send + // a "removed" event for the one we already have. + let index = this.actors.findIndex(a => { + let isSameType = a.player.constructor === player.constructor; + let isSameName = (a.isCssAnimation() && + a.player.animationName === player.animationName) || + (a.isCssTransition() && + a.player.transitionProperty === player.transitionProperty); + let isSameNode = a.player.effect.target === player.effect.target; + + return isSameType && isSameNode && isSameName; + }); + if (index !== -1) { + eventData.push({ + type: "removed", + player: this.actors[index] + }); + this.actors.splice(index, 1); + } + + let actor = AnimationPlayerActor(this, player); + this.actors.push(actor); + eventData.push({ + type: "added", + player: actor + }); + readyPromises.push(player.ready); + } + } + + if (eventData.length) { + // Let's wait for all added animations to be ready before telling the + // front-end. + Promise.all(readyPromises).then(() => { + events.emit(this, "mutations", eventData); + }); + } + }, + + /** + * After the client has called getAnimationPlayersForNode for a given DOM + * node, the actor starts sending animation mutations for this node. If the + * client doesn't want this to happen anymore, it should call this method. + */ + stopAnimationPlayerUpdates: function () { + if (this.observer && !Cu.isDeadWrapper(this.observer)) { + this.observer.disconnect(); + } + }, + + /** + * Iterates through all nodes below a given rootNode (optionally also in + * nested frames) and finds all existing animation players. + * @param {DOMNode} rootNode The root node to start iterating at. Animation + * players will *not* be reported for this node. + * @param {Boolean} traverseFrames Whether we should iterate through nested + * frames too. + * @return {Array} An array of AnimationPlayer objects. + */ + getAllAnimations: function (rootNode, traverseFrames) { + if (!traverseFrames) { + return rootNode.getAnimations({subtree: true}); + } + + let animations = []; + for (let {document} of this.tabActor.windows) { + animations = [...animations, ...document.getAnimations({subtree: true})]; + } + return animations; + }, + + onWillNavigate: function ({isTopLevel}) { + if (isTopLevel) { + this.stopAnimationPlayerUpdates(); + } + }, + + onNavigate: function ({isTopLevel}) { + if (isTopLevel) { + this.allAnimationsPaused = false; + } + }, + + /** + * Pause all animations in the current tabActor's frames. + */ + pauseAll: function () { + let readyPromises = []; + // Until the WebAnimations API provides a way to play/pause via the document + // timeline, we have to iterate through the whole DOM to find all players. + for (let player of + this.getAllAnimations(this.tabActor.window.document, true)) { + player.pause(); + readyPromises.push(player.ready); + } + this.allAnimationsPaused = true; + return promise.all(readyPromises); + }, + + /** + * Play all animations in the current tabActor's frames. + * This method only returns when animations have left their pending states. + */ + playAll: function () { + let readyPromises = []; + // Until the WebAnimations API provides a way to play/pause via the document + // timeline, we have to iterate through the whole DOM to find all players. + for (let player of + this.getAllAnimations(this.tabActor.window.document, true)) { + player.play(); + readyPromises.push(player.ready); + } + this.allAnimationsPaused = false; + return promise.all(readyPromises); + }, + + toggleAll: function () { + if (this.allAnimationsPaused) { + return this.playAll(); + } + return this.pauseAll(); + }, + + /** + * Toggle (play/pause) several animations at the same time. + * @param {Array} players A list of AnimationPlayerActor objects. + * @param {Boolean} shouldPause If set to true, the players will be paused, + * otherwise they will be played. + */ + toggleSeveral: function (players, shouldPause) { + return promise.all(players.map(player => { + return shouldPause ? player.pause() : player.play(); + })); + }, + + /** + * Set the current time of several animations at the same time. + * @param {Array} players A list of AnimationPlayerActor. + * @param {Number} time The new currentTime. + * @param {Boolean} shouldPause Should the players be paused too. + */ + setCurrentTimes: function (players, time, shouldPause) { + return promise.all(players.map(player => { + let pause = shouldPause ? player.pause() : promise.resolve(); + return pause.then(() => player.setCurrentTime(time)); + })); + }, + + /** + * Set the playback rate of several animations at the same time. + * @param {Array} players A list of AnimationPlayerActor. + * @param {Number} rate The new rate. + */ + setPlaybackRates: function (players, rate) { + for (let player of players) { + player.setPlaybackRate(rate); + } + } +}); diff --git a/devtools/server/actors/breakpoint.js b/devtools/server/actors/breakpoint.js new file mode 100644 index 000000000..547dcd0f1 --- /dev/null +++ b/devtools/server/actors/breakpoint.js @@ -0,0 +1,189 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { ActorClassWithSpec } = require("devtools/shared/protocol"); +const { breakpointSpec } = require("devtools/shared/specs/breakpoint"); + +/** + * Set breakpoints on all the given entry points with the given + * BreakpointActor as the handler. + * + * @param BreakpointActor actor + * The actor handling the breakpoint hits. + * @param Array entryPoints + * An array of objects of the form `{ script, offsets }`. + */ +function setBreakpointAtEntryPoints(actor, entryPoints) { + for (let { script, offsets } of entryPoints) { + actor.addScript(script); + for (let offset of offsets) { + script.setBreakpoint(offset, actor); + } + } +} + +exports.setBreakpointAtEntryPoints = setBreakpointAtEntryPoints; + +/** + * BreakpointActors exist for the lifetime of their containing thread and are + * responsible for deleting breakpoints, handling breakpoint hits and + * associating breakpoints with scripts. + */ +let BreakpointActor = ActorClassWithSpec(breakpointSpec, { + /** + * Create a Breakpoint actor. + * + * @param ThreadActor threadActor + * The parent thread actor that contains this breakpoint. + * @param OriginalLocation originalLocation + * The original location of the breakpoint. + */ + initialize: function (threadActor, originalLocation) { + // The set of Debugger.Script instances that this breakpoint has been set + // upon. + this.scripts = new Set(); + + this.threadActor = threadActor; + this.originalLocation = originalLocation; + this.condition = null; + this.isPending = true; + }, + + disconnect: function () { + this.removeScripts(); + }, + + hasScript: function (script) { + return this.scripts.has(script); + }, + + /** + * Called when this same breakpoint is added to another Debugger.Script + * instance. + * + * @param script Debugger.Script + * The new source script on which the breakpoint has been set. + */ + addScript: function (script) { + this.scripts.add(script); + this.isPending = false; + }, + + /** + * Remove the breakpoints from associated scripts and clear the script cache. + */ + removeScripts: function () { + for (let script of this.scripts) { + script.clearBreakpoint(this); + } + this.scripts.clear(); + }, + + /** + * Check if this breakpoint has a condition that doesn't error and + * evaluates to true in frame. + * + * @param frame Debugger.Frame + * The frame to evaluate the condition in + * @returns Object + * - result: boolean|undefined + * True when the conditional breakpoint should trigger a pause, + * false otherwise. If the condition evaluation failed/killed, + * `result` will be `undefined`. + * - message: string + * If the condition throws, this is the thrown message. + */ + checkCondition: function (frame) { + let completion = frame.eval(this.condition); + if (completion) { + if (completion.throw) { + // The evaluation failed and threw + let message = "Unknown exception"; + try { + if (completion.throw.getOwnPropertyDescriptor) { + message = completion.throw.getOwnPropertyDescriptor("message") + .value; + } else if (completion.toString) { + message = completion.toString(); + } + } catch (ex) {} + return { + result: true, + message: message + }; + } else if (completion.yield) { + assert(false, "Shouldn't ever get yield completions from an eval"); + } else { + return { result: completion.return ? true : false }; + } + } else { + // The evaluation was killed (possibly by the slow script dialog) + return { result: undefined }; + } + }, + + /** + * A function that the engine calls when a breakpoint has been hit. + * + * @param frame Debugger.Frame + * The stack frame that contained the breakpoint. + */ + hit: function (frame) { + // Don't pause if we are currently stepping (in or over) or the frame is + // black-boxed. + let generatedLocation = this.threadActor.sources.getFrameLocation(frame); + let { originalSourceActor } = this.threadActor.unsafeSynchronize( + this.threadActor.sources.getOriginalLocation(generatedLocation)); + let url = originalSourceActor.url; + + if (this.threadActor.sources.isBlackBoxed(url) + || frame.onStep) { + return undefined; + } + + let reason = {}; + + if (this.threadActor._hiddenBreakpoints.has(this.actorID)) { + reason.type = "pauseOnDOMEvents"; + } else if (!this.condition) { + reason.type = "breakpoint"; + // TODO: add the rest of the breakpoints on that line (bug 676602). + reason.actors = [ this.actorID ]; + } else { + let { result, message } = this.checkCondition(frame); + + if (result) { + if (!message) { + reason.type = "breakpoint"; + } else { + reason.type = "breakpointConditionThrown"; + reason.message = message; + } + reason.actors = [ this.actorID ]; + } else { + return undefined; + } + } + return this.threadActor._pauseAndRespond(frame, reason); + }, + + /** + * Handle a protocol request to remove this breakpoint. + */ + delete: function () { + // Remove from the breakpoint store. + if (this.originalLocation) { + this.threadActor.breakpointActorMap.deleteActor(this.originalLocation); + } + this.threadActor.threadLifetimePool.removeActor(this); + // Remove the actual breakpoint from the associated scripts. + this.removeScripts(); + } +}); + +exports.BreakpointActor = BreakpointActor; diff --git a/devtools/server/actors/call-watcher.js b/devtools/server/actors/call-watcher.js new file mode 100644 index 000000000..5729f9508 --- /dev/null +++ b/devtools/server/actors/call-watcher.js @@ -0,0 +1,634 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {Cc, Ci, Cu, Cr} = require("chrome"); +const events = require("sdk/event/core"); +const protocol = require("devtools/shared/protocol"); +const {serializeStack, parseStack} = require("toolkit/loader"); + +const {on, once, off, emit} = events; +const {method, Arg, Option, RetVal} = protocol; + +const { functionCallSpec, callWatcherSpec } = require("devtools/shared/specs/call-watcher"); +const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher"); + +/** + * This actor contains information about a function call, like the function + * type, name, stack, arguments, returned value etc. + */ +var FunctionCallActor = protocol.ActorClassWithSpec(functionCallSpec, { + /** + * Creates the function call actor. + * + * @param DebuggerServerConnection conn + * The server connection. + * @param DOMWindow window + * The content window. + * @param string global + * The name of the global object owning this function, like + * "CanvasRenderingContext2D" or "WebGLRenderingContext". + * @param object caller + * The object owning the function when it was called. + * For example, in `foo.bar()`, the caller is `foo`. + * @param number type + * Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER. + * @param string name + * The called function's name. + * @param array stack + * The called function's stack, as a list of { name, file, line } objects. + * @param number timestamp + * The performance.now() timestamp when the function was called. + * @param array args + * The called function's arguments. + * @param any result + * The value returned by the function call. + * @param boolean holdWeak + * Determines whether or not FunctionCallActor stores a weak reference + * to the underlying objects. + */ + initialize: function (conn, [window, global, caller, type, name, stack, timestamp, args, result], holdWeak) { + protocol.Actor.prototype.initialize.call(this, conn); + + this.details = { + global: global, + type: type, + name: name, + stack: stack, + timestamp: timestamp + }; + + // Store a weak reference to all objects so we don't + // prevent natural GC if `holdWeak` was passed into + // setup as truthy. + if (holdWeak) { + let weakRefs = { + window: Cu.getWeakReference(window), + caller: Cu.getWeakReference(caller), + args: Cu.getWeakReference(args), + result: Cu.getWeakReference(result), + }; + + Object.defineProperties(this.details, { + window: { get: () => weakRefs.window.get() }, + caller: { get: () => weakRefs.caller.get() }, + args: { get: () => weakRefs.args.get() }, + result: { get: () => weakRefs.result.get() }, + }); + } + // Otherwise, hold strong references to the objects. + else { + this.details.window = window; + this.details.caller = caller; + this.details.args = args; + this.details.result = result; + } + + // The caller, args and results are string names for now. It would + // certainly be nicer if they were Object actors. Make this smarter, so + // that the frontend can inspect each argument, be it object or primitive. + // Bug 978960. + this.details.previews = { + caller: this._generateStringPreview(caller), + args: this._generateArgsPreview(args), + result: this._generateStringPreview(result) + }; + }, + + /** + * Customize the marshalling of this actor to provide some generic information + * directly on the Front instance. + */ + form: function () { + return { + actor: this.actorID, + type: this.details.type, + name: this.details.name, + file: this.details.stack[0].file, + line: this.details.stack[0].line, + timestamp: this.details.timestamp, + callerPreview: this.details.previews.caller, + argsPreview: this.details.previews.args, + resultPreview: this.details.previews.result + }; + }, + + /** + * Gets more information about this function call, which is not necessarily + * available on the Front instance. + */ + getDetails: function () { + let { type, name, stack, timestamp } = this.details; + + // Since not all calls on the stack have corresponding owner files (e.g. + // callbacks of a requestAnimationFrame etc.), there's no benefit in + // returning them, as the user can't jump to the Debugger from them. + for (let i = stack.length - 1; ;) { + if (stack[i].file) { + break; + } + stack.pop(); + i--; + } + + // XXX: Use grips for objects and serialize them properly, in order + // to add the function's caller, arguments and return value. Bug 978957. + return { + type: type, + name: name, + stack: stack, + timestamp: timestamp + }; + }, + + /** + * Serializes the arguments so that they can be easily be transferred + * as a string, but still be useful when displayed in a potential UI. + * + * @param array args + * The source arguments. + * @return string + * The arguments as a string. + */ + _generateArgsPreview: function (args) { + let { global, name, caller } = this.details; + + // Get method signature to determine if there are any enums + // used in this method. + let methodSignatureEnums; + + let knownGlobal = CallWatcherFront.KNOWN_METHODS[global]; + if (knownGlobal) { + let knownMethod = knownGlobal[name]; + if (knownMethod) { + let isOverloaded = typeof knownMethod.enums === "function"; + if (isOverloaded) { + methodSignatureEnums = methodSignatureEnums(args); + } else { + methodSignatureEnums = knownMethod.enums; + } + } + } + + let serializeArgs = () => args.map((arg, i) => { + // XXX: Bug 978960. + if (arg === undefined) { + return "undefined"; + } + if (arg === null) { + return "null"; + } + if (typeof arg == "function") { + return "Function"; + } + if (typeof arg == "object") { + return "Object"; + } + // If this argument matches the method's signature + // and is an enum, change it to its constant name. + if (methodSignatureEnums && methodSignatureEnums.has(i)) { + return getBitToEnumValue(global, caller, arg); + } + return arg + ""; + }); + + return serializeArgs().join(", "); + }, + + /** + * Serializes the data so that it can be easily be transferred + * as a string, but still be useful when displayed in a potential UI. + * + * @param object data + * The source data. + * @return string + * The arguments as a string. + */ + _generateStringPreview: function (data) { + // XXX: Bug 978960. + if (data === undefined) { + return "undefined"; + } + if (data === null) { + return "null"; + } + if (typeof data == "function") { + return "Function"; + } + if (typeof data == "object") { + return "Object"; + } + return data + ""; + } +}); + +/** + * This actor observes function calls on certain objects or globals. + */ +var CallWatcherActor = exports.CallWatcherActor = protocol.ActorClassWithSpec(callWatcherSpec, { + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, conn); + this.tabActor = tabActor; + this._onGlobalCreated = this._onGlobalCreated.bind(this); + this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this); + this._onContentFunctionCall = this._onContentFunctionCall.bind(this); + on(this.tabActor, "window-ready", this._onGlobalCreated); + on(this.tabActor, "window-destroyed", this._onGlobalDestroyed); + }, + destroy: function (conn) { + protocol.Actor.prototype.destroy.call(this, conn); + off(this.tabActor, "window-ready", this._onGlobalCreated); + off(this.tabActor, "window-destroyed", this._onGlobalDestroyed); + this.finalize(); + }, + + /** + * Lightweight listener invoked whenever an instrumented function is called + * while recording. We're doing this to avoid the event emitter overhead, + * since this is expected to be a very hot function. + */ + onCall: null, + + /** + * Starts waiting for the current tab actor's document global to be + * created, in order to instrument the specified objects and become + * aware of everything the content does with them. + */ + setup: function ({ tracedGlobals, tracedFunctions, startRecording, performReload, holdWeak, storeCalls }) { + if (this._initialized) { + return; + } + this._initialized = true; + this._timestampEpoch = 0; + + this._functionCalls = []; + this._tracedGlobals = tracedGlobals || []; + this._tracedFunctions = tracedFunctions || []; + this._holdWeak = !!holdWeak; + this._storeCalls = !!storeCalls; + + if (startRecording) { + this.resumeRecording(); + } + if (performReload) { + this.tabActor.window.location.reload(); + } + }, + + /** + * Stops listening for document global changes and puts this actor + * to hibernation. This method is called automatically just before the + * actor is destroyed. + */ + finalize: function () { + if (!this._initialized) { + return; + } + this._initialized = false; + this._finalized = true; + + this._tracedGlobals = null; + this._tracedFunctions = null; + }, + + /** + * Returns whether the instrumented function calls are currently recorded. + */ + isRecording: function () { + return this._recording; + }, + + /** + * Initialize the timestamp epoch used to offset function call timestamps. + */ + initTimestampEpoch: function () { + this._timestampEpoch = this.tabActor.window.performance.now(); + }, + + /** + * Starts recording function calls. + */ + resumeRecording: function () { + this._recording = true; + }, + + /** + * Stops recording function calls. + */ + pauseRecording: function () { + this._recording = false; + return this._functionCalls; + }, + + /** + * Erases all the recorded function calls. + * Calling `resumeRecording` or `pauseRecording` does not erase history. + */ + eraseRecording: function () { + this._functionCalls = []; + }, + + /** + * Invoked whenever the current tab actor's document global is created. + */ + _onGlobalCreated: function ({window, id, isTopLevel}) { + if (!this._initialized) { + return; + } + + // TODO: bug 981748, support more than just the top-level documents. + if (!isTopLevel) { + return; + } + + let self = this; + this._tracedWindowId = id; + + let unwrappedWindow = XPCNativeWrapper.unwrap(window); + let callback = this._onContentFunctionCall; + + for (let global of this._tracedGlobals) { + let prototype = unwrappedWindow[global].prototype; + let properties = Object.keys(prototype); + properties.forEach(name => overrideSymbol(global, prototype, name, callback)); + } + + for (let name of this._tracedFunctions) { + overrideSymbol("window", unwrappedWindow, name, callback); + } + + /** + * Instruments a method, getter or setter on the specified target object to + * invoke a callback whenever it is called. + */ + function overrideSymbol(global, target, name, callback) { + let propertyDescriptor = Object.getOwnPropertyDescriptor(target, name); + + if (propertyDescriptor.get || propertyDescriptor.set) { + overrideAccessor(global, target, name, propertyDescriptor, callback); + return; + } + if (propertyDescriptor.writable && typeof propertyDescriptor.value == "function") { + overrideFunction(global, target, name, propertyDescriptor, callback); + return; + } + } + + /** + * Instruments a function on the specified target object. + */ + function overrideFunction(global, target, name, descriptor, callback) { + // Invoking .apply on an unxrayed content function doesn't work, because + // the arguments array is inaccessible to it. Get Xrays back. + let originalFunc = Cu.unwaiveXrays(target[name]); + + Cu.exportFunction(function (...args) { + let result; + try { + result = Cu.waiveXrays(originalFunc.apply(this, args)); + } catch (e) { + throw createContentError(e, unwrappedWindow); + } + + if (self._recording) { + let type = CallWatcherFront.METHOD_FUNCTION; + let stack = getStack(name); + let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch; + callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result); + } + return result; + }, target, { defineAs: name }); + + Object.defineProperty(target, name, { + configurable: descriptor.configurable, + enumerable: descriptor.enumerable, + writable: true + }); + } + + /** + * Instruments a getter or setter on the specified target object. + */ + function overrideAccessor(global, target, name, descriptor, callback) { + // Invoking .apply on an unxrayed content function doesn't work, because + // the arguments array is inaccessible to it. Get Xrays back. + let originalGetter = Cu.unwaiveXrays(target.__lookupGetter__(name)); + let originalSetter = Cu.unwaiveXrays(target.__lookupSetter__(name)); + + Object.defineProperty(target, name, { + get: function (...args) { + if (!originalGetter) return undefined; + let result = Cu.waiveXrays(originalGetter.apply(this, args)); + + if (self._recording) { + let type = CallWatcherFront.GETTER_FUNCTION; + let stack = getStack(name); + let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch; + callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, result); + } + return result; + }, + set: function (...args) { + if (!originalSetter) return; + originalSetter.apply(this, args); + + if (self._recording) { + let type = CallWatcherFront.SETTER_FUNCTION; + let stack = getStack(name); + let timestamp = self.tabActor.window.performance.now() - self._timestampEpoch; + callback(unwrappedWindow, global, this, type, name, stack, timestamp, args, undefined); + } + }, + configurable: descriptor.configurable, + enumerable: descriptor.enumerable + }); + } + + /** + * Stores the relevant information about calls on the stack when + * a function is called. + */ + function getStack(caller) { + try { + // Using Components.stack wouldn't be a better idea, since it's + // much slower because it attempts to retrieve the C++ stack as well. + throw new Error(); + } catch (e) { + var stack = e.stack; + } + + // Of course, using a simple regex like /(.*?)@(.*):(\d*):\d*/ would be + // much prettier, but this is a very hot function, so let's sqeeze + // every drop of performance out of it. + let calls = []; + let callIndex = 0; + let currNewLinePivot = stack.indexOf("\n") + 1; + let nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); + + while (nextNewLinePivot > 0) { + let nameDelimiterIndex = stack.indexOf("@", currNewLinePivot); + let columnDelimiterIndex = stack.lastIndexOf(":", nextNewLinePivot - 1); + let lineDelimiterIndex = stack.lastIndexOf(":", columnDelimiterIndex - 1); + + if (!calls[callIndex]) { + calls[callIndex] = { name: "", file: "", line: 0 }; + } + if (!calls[callIndex + 1]) { + calls[callIndex + 1] = { name: "", file: "", line: 0 }; + } + + if (callIndex > 0) { + let file = stack.substring(nameDelimiterIndex + 1, lineDelimiterIndex); + let line = stack.substring(lineDelimiterIndex + 1, columnDelimiterIndex); + let name = stack.substring(currNewLinePivot, nameDelimiterIndex); + calls[callIndex].name = name; + calls[callIndex - 1].file = file; + calls[callIndex - 1].line = line; + } else { + // Since the topmost stack frame is actually our overwritten function, + // it will not have the expected name. + calls[0].name = caller; + } + + currNewLinePivot = nextNewLinePivot + 1; + nextNewLinePivot = stack.indexOf("\n", currNewLinePivot); + callIndex++; + } + + return calls; + } + }, + + /** + * Invoked whenever the current tab actor's inner window is destroyed. + */ + _onGlobalDestroyed: function ({window, id, isTopLevel}) { + if (this._tracedWindowId == id) { + this.pauseRecording(); + this.eraseRecording(); + this._timestampEpoch = 0; + } + }, + + /** + * Invoked whenever an instrumented function is called. + */ + _onContentFunctionCall: function (...details) { + // If the consuming tool has finalized call-watcher, ignore the + // still-instrumented calls. + if (this._finalized) { + return; + } + + let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak); + + if (this._storeCalls) { + this._functionCalls.push(functionCall); + } + + if (this.onCall) { + this.onCall(functionCall); + } else { + emit(this, "call", functionCall); + } + } +}); + +/** + * A lookup table for cross-referencing flags or properties with their name + * assuming they look LIKE_THIS most of the time. + * + * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed + * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT". + */ +var gEnumRegex = /^[A-Z][A-Z0-9_]+$/; +var gEnumsLookupTable = {}; + +// These values are returned from errors, or empty values, +// and need to be ignored when checking arguments due to the bitwise math. +var INVALID_ENUMS = [ + "INVALID_ENUM", "NO_ERROR", "INVALID_VALUE", "OUT_OF_MEMORY", "NONE" +]; + +function getBitToEnumValue(type, object, arg) { + let table = gEnumsLookupTable[type]; + + // If mapping not yet created, do it on the first run. + if (!table) { + table = gEnumsLookupTable[type] = {}; + + for (let key in object) { + if (key.match(gEnumRegex)) { + // Maps `16384` to `"COLOR_BUFFER_BIT"`, etc. + table[object[key]] = key; + } + } + } + + // If a single bit value, just return it. + if (table[arg]) { + return table[arg]; + } + + // Otherwise, attempt to reduce it to the original bit flags: + // `16640` -> "COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT" + let flags = []; + for (let flag in table) { + if (INVALID_ENUMS.indexOf(table[flag]) !== -1) { + continue; + } + + // Cast to integer as all values are stored as strings + // in `table` + flag = flag | 0; + if (flag && (arg & flag) === flag) { + flags.push(table[flag]); + } + } + + // Cache the combined bitmask value + return table[arg] = flags.join(" | ") || arg; +} + +/** + * Creates a new error from an error that originated from content but was called + * from a wrapped overridden method. This is so we can make our own error + * that does not look like it originated from the call watcher. + * + * We use toolkit/loader's parseStack and serializeStack rather than the + * parsing done in the local `getStack` function, because it does not expose + * column number, would have to change the protocol models `call-stack-items` and `call-details` + * which hurts backwards compatibility, and the local `getStack` is an optimized, hot function. + */ +function createContentError(e, win) { + let { message, name, stack } = e; + let parsedStack = parseStack(stack); + let { fileName, lineNumber, columnNumber } = parsedStack[parsedStack.length - 1]; + let error; + + let isDOMException = e instanceof Ci.nsIDOMDOMException; + let constructor = isDOMException ? win.DOMException : (win[e.name] || win.Error); + + if (isDOMException) { + error = new constructor(message, name); + Object.defineProperties(error, { + code: { value: e.code }, + columnNumber: { value: 0 }, // columnNumber is always 0 for DOMExceptions? + filename: { value: fileName }, // note the lowercase `filename` + lineNumber: { value: lineNumber }, + result: { value: e.result }, + stack: { value: serializeStack(parsedStack) } + }); + } + else { + // Constructing an error here retains all the stack information, + // and we can add message, fileName and lineNumber via constructor, though + // need to manually add columnNumber. + error = new constructor(message, fileName, lineNumber); + Object.defineProperty(error, "columnNumber", { + value: columnNumber + }); + } + return error; +} diff --git a/devtools/server/actors/canvas.js b/devtools/server/actors/canvas.js new file mode 100644 index 000000000..f6e1f57ec --- /dev/null +++ b/devtools/server/actors/canvas.js @@ -0,0 +1,728 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {Cc, Ci, Cu, Cr} = require("chrome"); +const events = require("sdk/event/core"); +const promise = require("promise"); +const protocol = require("devtools/shared/protocol"); +const {CallWatcherActor} = require("devtools/server/actors/call-watcher"); +const {CallWatcherFront} = require("devtools/shared/fronts/call-watcher"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const {WebGLPrimitiveCounter} = require("devtools/server/primitive"); +const { + frameSnapshotSpec, + canvasSpec, + CANVAS_CONTEXTS, + ANIMATION_GENERATORS, + LOOP_GENERATORS, + DRAW_CALLS, + INTERESTING_CALLS, +} = require("devtools/shared/specs/canvas"); +const {CanvasFront} = require("devtools/shared/fronts/canvas"); + +const {on, once, off, emit} = events; +const {method, custom, Arg, Option, RetVal} = protocol; + +/** + * This actor represents a recorded animation frame snapshot, along with + * all the corresponding canvas' context methods invoked in that frame, + * thumbnails for each draw call and a screenshot of the end result. + */ +var FrameSnapshotActor = protocol.ActorClassWithSpec(frameSnapshotSpec, { + /** + * Creates the frame snapshot call actor. + * + * @param DebuggerServerConnection conn + * The server connection. + * @param HTMLCanvasElement canvas + * A reference to the content canvas. + * @param array calls + * An array of "function-call" actor instances. + * @param object screenshot + * A single "snapshot-image" type instance. + */ + initialize: function (conn, { canvas, calls, screenshot, primitive }) { + protocol.Actor.prototype.initialize.call(this, conn); + this._contentCanvas = canvas; + this._functionCalls = calls; + this._animationFrameEndScreenshot = screenshot; + this._primitive = primitive; + }, + + /** + * Gets as much data about this snapshot without computing anything costly. + */ + getOverview: function () { + return { + calls: this._functionCalls, + thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e), + screenshot: this._animationFrameEndScreenshot, + primitive: { + tris: this._primitive.tris, + vertices: this._primitive.vertices, + points: this._primitive.points, + lines: this._primitive.lines + } + }; + }, + + /** + * Gets a screenshot of the canvas's contents after the specified + * function was called. + */ + generateScreenshotFor: function (functionCall) { + let caller = functionCall.details.caller; + let global = functionCall.details.global; + + let canvas = this._contentCanvas; + let calls = this._functionCalls; + let index = calls.indexOf(functionCall); + + // To get a screenshot, replay all the steps necessary to render the frame, + // by invoking the context calls up to and including the specified one. + // This will be done in a custom framebuffer in case of a WebGL context. + let replayData = ContextUtils.replayAnimationFrame({ + contextType: global, + canvas: canvas, + calls: calls, + first: 0, + last: index + }); + + let { replayContext, replayContextScaling, lastDrawCallIndex, doCleanup } = replayData; + let [left, top, width, height] = replayData.replayViewport; + let screenshot; + + // Depending on the canvas' context, generating a screenshot is done + // in different ways. + if (global == "WebGLRenderingContext") { + screenshot = ContextUtils.getPixelsForWebGL(replayContext, left, top, width, height); + screenshot.flipped = true; + } else if (global == "CanvasRenderingContext2D") { + screenshot = ContextUtils.getPixelsFor2D(replayContext, left, top, width, height); + screenshot.flipped = false; + } + + // In case of the WebGL context, we also need to reset the framebuffer + // binding to the original value, after generating the screenshot. + doCleanup(); + + screenshot.scaling = replayContextScaling; + screenshot.index = lastDrawCallIndex; + return screenshot; + } +}); + +/** + * This Canvas Actor handles simple instrumentation of all the methods + * of a 2D or WebGL context, to provide information regarding all the calls + * made when drawing frame inside an animation loop. + */ +var CanvasActor = exports.CanvasActor = protocol.ActorClassWithSpec(canvasSpec, { + // Reset for each recording, boolean indicating whether or not + // any draw calls were called for a recording. + _animationContainsDrawCall: false, + + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, conn); + this.tabActor = tabActor; + this._webGLPrimitiveCounter = new WebGLPrimitiveCounter(tabActor); + this._onContentFunctionCall = this._onContentFunctionCall.bind(this); + }, + destroy: function (conn) { + protocol.Actor.prototype.destroy.call(this, conn); + this._webGLPrimitiveCounter.destroy(); + this.finalize(); + }, + + /** + * Starts listening for function calls. + */ + setup: function ({ reload }) { + if (this._initialized) { + if (reload) { + this.tabActor.window.location.reload(); + } + return; + } + this._initialized = true; + + this._callWatcher = new CallWatcherActor(this.conn, this.tabActor); + this._callWatcher.onCall = this._onContentFunctionCall; + this._callWatcher.setup({ + tracedGlobals: CANVAS_CONTEXTS, + tracedFunctions: [...ANIMATION_GENERATORS, ...LOOP_GENERATORS], + performReload: reload, + storeCalls: true + }); + }, + + /** + * Stops listening for function calls. + */ + finalize: function () { + if (!this._initialized) { + return; + } + this._initialized = false; + + this._callWatcher.finalize(); + this._callWatcher = null; + }, + + /** + * Returns whether this actor has been set up. + */ + isInitialized: function () { + return !!this._initialized; + }, + + /** + * Returns whether or not the CanvasActor is recording an animation. + * Used in tests. + */ + isRecording: function () { + return !!this._callWatcher.isRecording(); + }, + + /** + * Records a snapshot of all the calls made during the next animation frame. + * The animation should be implemented via the de-facto requestAnimationFrame + * utility, or inside recursive `setTimeout`s. `setInterval` at this time are not supported. + */ + recordAnimationFrame: function () { + if (this._callWatcher.isRecording()) { + return this._currentAnimationFrameSnapshot.promise; + } + + this._recordingContainsDrawCall = false; + this._callWatcher.eraseRecording(); + this._callWatcher.initTimestampEpoch(); + this._webGLPrimitiveCounter.resetCounts(); + this._callWatcher.resumeRecording(); + + let deferred = this._currentAnimationFrameSnapshot = promise.defer(); + return deferred.promise; + }, + + /** + * Cease attempts to record an animation frame. + */ + stopRecordingAnimationFrame: function () { + if (!this._callWatcher.isRecording()) { + return; + } + this._animationStarted = false; + this._callWatcher.pauseRecording(); + this._callWatcher.eraseRecording(); + this._currentAnimationFrameSnapshot.resolve(null); + this._currentAnimationFrameSnapshot = null; + }, + + /** + * Invoked whenever an instrumented function is called, be it on a + * 2d or WebGL context, or an animation generator like requestAnimationFrame. + */ + _onContentFunctionCall: function (functionCall) { + let { window, name, args } = functionCall.details; + + // The function call arguments are required to replay animation frames, + // in order to generate screenshots. However, simply storing references to + // every kind of object is a bad idea, since their properties may change. + // Consider transformation matrices for example, which are typically + // Float32Arrays whose values can easily change across context calls. + // They need to be cloned. + inplaceShallowCloneArrays(args, window); + + // Handle animations generated using requestAnimationFrame + if (CanvasFront.ANIMATION_GENERATORS.has(name)) { + this._handleAnimationFrame(functionCall); + return; + } + // Handle animations generated using setTimeout. While using + // those timers is considered extremely poor practice, they're still widely + // used on the web, especially for old demos; it's nice to support them as well. + if (CanvasFront.LOOP_GENERATORS.has(name)) { + this._handleAnimationFrame(functionCall); + return; + } + if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) { + this._handleDrawCall(functionCall); + this._webGLPrimitiveCounter.handleDrawPrimitive(functionCall); + return; + } + }, + + /** + * Handle animations generated using requestAnimationFrame. + */ + _handleAnimationFrame: function (functionCall) { + if (!this._animationStarted) { + this._handleAnimationFrameBegin(); + } + // Check to see if draw calls occurred yet, as it could be future frames, + // like in the scenario where requestAnimationFrame is called to trigger an animation, + // and rAF is at the beginning of the animate loop. + else if (this._animationContainsDrawCall) { + this._handleAnimationFrameEnd(functionCall); + } + }, + + /** + * Called whenever an animation frame rendering begins. + */ + _handleAnimationFrameBegin: function () { + this._callWatcher.eraseRecording(); + this._animationStarted = true; + }, + + /** + * Called whenever an animation frame rendering ends. + */ + _handleAnimationFrameEnd: function () { + // Get a hold of all the function calls made during this animation frame. + // Since only one snapshot can be recorded at a time, erase all the + // previously recorded calls. + let functionCalls = this._callWatcher.pauseRecording(); + this._callWatcher.eraseRecording(); + this._animationContainsDrawCall = false; + + // Since the animation frame finished, get a hold of the (already retrieved) + // canvas pixels to conveniently create a screenshot of the final rendering. + let index = this._lastDrawCallIndex; + let width = this._lastContentCanvasWidth; + let height = this._lastContentCanvasHeight; + let flipped = !!this._lastThumbnailFlipped; // undefined -> false + let pixels = ContextUtils.getPixelStorage()["8bit"]; + let primitiveResult = this._webGLPrimitiveCounter.getCounts(); + let animationFrameEndScreenshot = { + index: index, + width: width, + height: height, + scaling: 1, + flipped: flipped, + pixels: pixels.subarray(0, width * height * 4) + }; + + // Wrap the function calls and screenshot in a FrameSnapshotActor instance, + // which will resolve the promise returned by `recordAnimationFrame`. + let frameSnapshot = new FrameSnapshotActor(this.conn, { + canvas: this._lastDrawCallCanvas, + calls: functionCalls, + screenshot: animationFrameEndScreenshot, + primitive: { + tris: primitiveResult.tris, + vertices: primitiveResult.vertices, + points: primitiveResult.points, + lines: primitiveResult.lines + } + }); + + this._currentAnimationFrameSnapshot.resolve(frameSnapshot); + this._currentAnimationFrameSnapshot = null; + this._animationStarted = false; + }, + + /** + * Invoked whenever a draw call is detected in the animation frame which is + * currently being recorded. + */ + _handleDrawCall: function (functionCall) { + let functionCalls = this._callWatcher.pauseRecording(); + let caller = functionCall.details.caller; + let global = functionCall.details.global; + + let contentCanvas = this._lastDrawCallCanvas = caller.canvas; + let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall); + let w = this._lastContentCanvasWidth = contentCanvas.width; + let h = this._lastContentCanvasHeight = contentCanvas.height; + + // To keep things fast, generate images of small and fixed dimensions. + let dimensions = CanvasFront.THUMBNAIL_SIZE; + let thumbnail; + + this._animationContainsDrawCall = true; + + // Create a thumbnail on every draw call on the canvas context, to augment + // the respective function call actor with this additional data. + if (global == "WebGLRenderingContext") { + // Check if drawing to a custom framebuffer (when rendering to texture). + // Don't create a thumbnail in this particular case. + let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING); + if (framebufferBinding == null) { + thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions); + thumbnail.flipped = this._lastThumbnailFlipped = true; + thumbnail.index = index; + } + } else if (global == "CanvasRenderingContext2D") { + thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions); + thumbnail.flipped = this._lastThumbnailFlipped = false; + thumbnail.index = index; + } + + functionCall._thumbnail = thumbnail; + this._callWatcher.resumeRecording(); + } +}); + +/** + * A collection of methods for manipulating canvas contexts. + */ +var ContextUtils = { + /** + * WebGL contexts are sensitive to how they're queried. Use this function + * to make sure the right context is always retrieved, if available. + * + * @param HTMLCanvasElement canvas + * The canvas element for which to get a WebGL context. + * @param WebGLRenderingContext gl + * The queried WebGL context, or null if unavailable. + */ + getWebGLContext: function (canvas) { + return canvas.getContext("webgl") || + canvas.getContext("experimental-webgl"); + }, + + /** + * Gets a hold of the rendered pixels in the most efficient way possible for + * a canvas with a WebGL context. + * + * @param WebGLRenderingContext gl + * The WebGL context to get a screenshot from. + * @param number srcX [optional] + * The first left pixel that is read from the framebuffer. + * @param number srcY [optional] + * The first top pixel that is read from the framebuffer. + * @param number srcWidth [optional] + * The number of pixels to read on the X axis. + * @param number srcHeight [optional] + * The number of pixels to read on the Y axis. + * @param number dstHeight [optional] + * The desired generated screenshot height. + * @return object + * An objet containing the screenshot's width, height and pixel data, + * represented as an 8-bit array buffer of r, g, b, a values. + */ + getPixelsForWebGL: function (gl, + srcX = 0, srcY = 0, + srcWidth = gl.canvas.width, + srcHeight = gl.canvas.height, + dstHeight = srcHeight) + { + let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight); + let { "8bit": charView, "32bit": intView } = contentPixels; + gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView); + return this.resizePixels(intView, srcWidth, srcHeight, dstHeight); + }, + + /** + * Gets a hold of the rendered pixels in the most efficient way possible for + * a canvas with a 2D context. + * + * @param CanvasRenderingContext2D ctx + * The 2D context to get a screenshot from. + * @param number srcX [optional] + * The first left pixel that is read from the canvas. + * @param number srcY [optional] + * The first top pixel that is read from the canvas. + * @param number srcWidth [optional] + * The number of pixels to read on the X axis. + * @param number srcHeight [optional] + * The number of pixels to read on the Y axis. + * @param number dstHeight [optional] + * The desired generated screenshot height. + * @return object + * An objet containing the screenshot's width, height and pixel data, + * represented as an 8-bit array buffer of r, g, b, a values. + */ + getPixelsFor2D: function (ctx, + srcX = 0, srcY = 0, + srcWidth = ctx.canvas.width, + srcHeight = ctx.canvas.height, + dstHeight = srcHeight) + { + let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight); + let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer); + return this.resizePixels(intView, srcWidth, srcHeight, dstHeight); + }, + + /** + * Resizes the provided pixels to fit inside a rectangle with the specified + * height and the same aspect ratio as the source. + * + * @param Uint32Array srcPixels + * The source pixel data, assuming 32bit/pixel and 4 color components. + * @param number srcWidth + * The source pixel data width. + * @param number srcHeight + * The source pixel data height. + * @param number dstHeight [optional] + * The desired resized pixel data height. + * @return object + * An objet containing the resized pixels width, height and data, + * represented as an 8-bit array buffer of r, g, b, a values. + */ + resizePixels: function (srcPixels, srcWidth, srcHeight, dstHeight) { + let screenshotRatio = dstHeight / srcHeight; + let dstWidth = (srcWidth * screenshotRatio) | 0; + let dstPixels = new Uint32Array(dstWidth * dstHeight); + + // If the resized image ends up being completely transparent, returning + // an empty array will skip some redundant serialization cycles. + let isTransparent = true; + + for (let dstX = 0; dstX < dstWidth; dstX++) { + for (let dstY = 0; dstY < dstHeight; dstY++) { + let srcX = (dstX / screenshotRatio) | 0; + let srcY = (dstY / screenshotRatio) | 0; + let cPos = srcX + srcWidth * srcY; + let dPos = dstX + dstWidth * dstY; + let color = dstPixels[dPos] = srcPixels[cPos]; + if (color) { + isTransparent = false; + } + } + } + + return { + width: dstWidth, + height: dstHeight, + pixels: isTransparent ? [] : new Uint8Array(dstPixels.buffer) + }; + }, + + /** + * Invokes a series of canvas context calls, to "replay" an animation frame + * and generate a screenshot. + * + * In case of a WebGL context, an offscreen framebuffer is created for + * the respective canvas, and the rendering will be performed into it. + * This is necessary because some state (like shaders, textures etc.) can't + * be shared between two different WebGL contexts. + * - Hopefully, once SharedResources are a thing this won't be necessary: + * http://www.khronos.org/webgl/wiki/SharedResouces + * - Alternatively, we could pursue the idea of using the same context + * for multiple canvases, instead of trying to share resources: + * https://www.khronos.org/webgl/public-mailing-list/archives/1210/msg00058.html + * + * In case of a 2D context, a new canvas is created, since there's no + * intrinsic state that can't be easily duplicated. + * + * @param number contexType + * The type of context to use. See the CallWatcherFront scope types. + * @param HTMLCanvasElement canvas + * The canvas element which is the source of all context calls. + * @param array calls + * An array of function call actors. + * @param number first + * The first function call to start from. + * @param number last + * The last (inclusive) function call to end at. + * @return object + * The context on which the specified calls were invoked, the + * last registered draw call's index and a cleanup function, which + * needs to be called whenever any potential followup work is finished. + */ + replayAnimationFrame: function ({ contextType, canvas, calls, first, last }) { + let w = canvas.width; + let h = canvas.height; + + let replayContext; + let replayContextScaling; + let customViewport; + let customFramebuffer; + let lastDrawCallIndex = -1; + let doCleanup = () => {}; + + // In case of WebGL contexts, rendering will be done offscreen, in a + // custom framebuffer, but using the same provided context. This is + // necessary because it's very memory-unfriendly to rebuild all the + // required GL state (like recompiling shaders, setting global flags, etc.) + // in an entirely new canvas. However, special care is needed to not + // permanently affect the existing GL state in the process. + if (contextType == "WebGLRenderingContext") { + // To keep things fast, replay the context calls on a framebuffer + // of smaller dimensions than the actual canvas (maximum 256x256 pixels). + let scaling = Math.min(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, h) / h; + replayContextScaling = scaling; + w = (w * scaling) | 0; + h = (h * scaling) | 0; + + // Fetch the same WebGL context and bind a new framebuffer. + let gl = replayContext = this.getWebGLContext(canvas); + let { newFramebuffer, oldFramebuffer } = this.createBoundFramebuffer(gl, w, h); + customFramebuffer = newFramebuffer; + + // Set the viewport to match the new framebuffer's dimensions. + let { newViewport, oldViewport } = this.setCustomViewport(gl, w, h); + customViewport = newViewport; + + // Revert the framebuffer and viewport to the original values. + doCleanup = () => { + gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer); + gl.viewport.apply(gl, oldViewport); + }; + } + // In case of 2D contexts, draw everything on a separate canvas context. + else if (contextType == "CanvasRenderingContext2D") { + let contentDocument = canvas.ownerDocument; + let replayCanvas = contentDocument.createElement("canvas"); + replayCanvas.width = w; + replayCanvas.height = h; + replayContext = replayCanvas.getContext("2d"); + replayContextScaling = 1; + customViewport = [0, 0, w, h]; + } + + // Replay all the context calls up to and including the specified one. + for (let i = first; i <= last; i++) { + let { type, name, args } = calls[i].details; + + // Prevent WebGL context calls that try to reset the framebuffer binding + // to the default value, since we want to perform the rendering offscreen. + if (name == "bindFramebuffer" && args[1] == null) { + replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer); + continue; + } + // Also prevent WebGL context calls that try to change the viewport + // while our custom framebuffer is bound. + if (name == "viewport") { + let framebufferBinding = replayContext.getParameter(replayContext.FRAMEBUFFER_BINDING); + if (framebufferBinding == customFramebuffer) { + replayContext.viewport.apply(replayContext, customViewport); + continue; + } + } + if (type == CallWatcherFront.METHOD_FUNCTION) { + replayContext[name].apply(replayContext, args); + } else if (type == CallWatcherFront.SETTER_FUNCTION) { + replayContext[name] = args; + } + if (CanvasFront.DRAW_CALLS.has(name)) { + lastDrawCallIndex = i; + } + } + + return { + replayContext: replayContext, + replayContextScaling: replayContextScaling, + replayViewport: customViewport, + lastDrawCallIndex: lastDrawCallIndex, + doCleanup: doCleanup + }; + }, + + /** + * Gets an object containing a buffer large enough to hold width * height + * pixels, assuming 32bit/pixel and 4 color components. + * + * This method avoids allocating memory and tries to reuse a common buffer + * as much as possible. + * + * @param number w + * The desired pixel array storage width. + * @param number h + * The desired pixel array storage height. + * @return object + * The requested pixel array buffer. + */ + getPixelStorage: function (w = 0, h = 0) { + let storage = this._currentPixelStorage; + if (storage && storage["32bit"].length >= w * h) { + return storage; + } + return this.usePixelStorage(new ArrayBuffer(w * h * 4)); + }, + + /** + * Creates and saves the array buffer views used by `getPixelStorage`. + * + * @param ArrayBuffer buffer + * The raw buffer used as storage for various array buffer views. + */ + usePixelStorage: function (buffer) { + let array8bit = new Uint8Array(buffer); + let array32bit = new Uint32Array(buffer); + return this._currentPixelStorage = { + "8bit": array8bit, + "32bit": array32bit + }; + }, + + /** + * Creates a framebuffer of the specified dimensions for a WebGL context, + * assuming a RGBA color buffer, a depth buffer and no stencil buffer. + * + * @param WebGLRenderingContext gl + * The WebGL context to create and bind a framebuffer for. + * @param number width + * The desired width of the renderbuffers. + * @param number height + * The desired height of the renderbuffers. + * @return WebGLFramebuffer + * The generated framebuffer object. + */ + createBoundFramebuffer: function (gl, width, height) { + let oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + let oldRenderbufferBinding = gl.getParameter(gl.RENDERBUFFER_BINDING); + let oldTextureBinding = gl.getParameter(gl.TEXTURE_BINDING_2D); + + let newFramebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, newFramebuffer); + + // Use a texture as the color renderbuffer attachment, since consumers of + // this function will most likely want to read the rendered pixels back. + let colorBuffer = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, colorBuffer); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + let depthBuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); + + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); + + gl.bindTexture(gl.TEXTURE_2D, oldTextureBinding); + gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbufferBinding); + + return { oldFramebuffer, newFramebuffer }; + }, + + /** + * Sets the viewport of the drawing buffer for a WebGL context. + * @param WebGLRenderingContext gl + * @param number width + * @param number height + */ + setCustomViewport: function (gl, width, height) { + let oldViewport = XPCNativeWrapper.unwrap(gl.getParameter(gl.VIEWPORT)); + let newViewport = [0, 0, width, height]; + gl.viewport.apply(gl, newViewport); + + return { oldViewport, newViewport }; + } +}; + +/** + * Goes through all the arguments and creates a one-level shallow copy + * of all arrays and array buffers. + */ +function inplaceShallowCloneArrays(functionArguments, contentWindow) { + let { Object, Array, ArrayBuffer } = contentWindow; + + functionArguments.forEach((arg, index, store) => { + if (arg instanceof Array) { + store[index] = arg.slice(); + } + if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) { + store[index] = new arg.constructor(arg); + } + }); +} diff --git a/devtools/server/actors/child-process.js b/devtools/server/actors/child-process.js new file mode 100644 index 000000000..7b0e2eaf8 --- /dev/null +++ b/devtools/server/actors/child-process.js @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci, Cu } = require("chrome"); + +const { ChromeDebuggerActor } = require("devtools/server/actors/script"); +const { WebConsoleActor } = require("devtools/server/actors/webconsole"); +const makeDebugger = require("devtools/server/actors/utils/make-debugger"); +const { ActorPool } = require("devtools/server/main"); +const Services = require("Services"); +const { assert } = require("devtools/shared/DevToolsUtils"); +const { TabSources } = require("./utils/TabSources"); + +loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true); + +function ChildProcessActor(aConnection) { + this.conn = aConnection; + this._contextPool = new ActorPool(this.conn); + this.conn.addActorPool(this._contextPool); + this.threadActor = null; + + // Use a see-everything debugger + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: dbg => dbg.findAllGlobals(), + shouldAddNewGlobalAsDebuggee: global => true + }); + + // Scope into which the webconsole executes: + // An empty sandbox with chrome privileges + let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + let sandbox = Cu.Sandbox(systemPrincipal); + this._consoleScope = sandbox; + + this._workerList = null; + this._workerActorPool = null; + this._onWorkerListChanged = this._onWorkerListChanged.bind(this); +} +exports.ChildProcessActor = ChildProcessActor; + +ChildProcessActor.prototype = { + actorPrefix: "process", + + get isRootActor() { + return true; + }, + + get exited() { + return !this._contextPool; + }, + + get url() { + return undefined; + }, + + get window() { + return this._consoleScope; + }, + + get sources() { + if (!this._sources) { + assert(this.threadActor, "threadActor should exist when creating sources."); + this._sources = new TabSources(this.threadActor); + } + return this._sources; + }, + + form: function () { + if (!this._consoleActor) { + this._consoleActor = new WebConsoleActor(this.conn, this); + this._contextPool.addActor(this._consoleActor); + } + + if (!this.threadActor) { + this.threadActor = new ChromeDebuggerActor(this.conn, this); + this._contextPool.addActor(this.threadActor); + } + + return { + actor: this.actorID, + name: "Content process", + + consoleActor: this._consoleActor.actorID, + chromeDebugger: this.threadActor.actorID, + + traits: { + highlightable: false, + networkMonitor: false, + }, + }; + }, + + onListWorkers: function () { + if (!this._workerList) { + this._workerList = new WorkerActorList(this.conn, {}); + } + return this._workerList.getList().then(actors => { + let pool = new ActorPool(this.conn); + for (let actor of actors) { + pool.addActor(actor); + } + + this.conn.removeActorPool(this._workerActorPool); + this._workerActorPool = pool; + this.conn.addActorPool(this._workerActorPool); + + this._workerList.onListChanged = this._onWorkerListChanged; + + return { + "from": this.actorID, + "workers": actors.map(actor => actor.form()) + }; + }); + }, + + _onWorkerListChanged: function () { + this.conn.send({ from: this.actorID, type: "workerListChanged" }); + this._workerList.onListChanged = null; + }, + + disconnect: function () { + this.conn.removeActorPool(this._contextPool); + this._contextPool = null; + + // Tell the live lists we aren't watching any more. + if (this._workerList) { + this._workerList.onListChanged = null; + } + }, + + preNest: function () { + // TODO: freeze windows + // window mediator doesn't work in child. + // it doesn't throw, but doesn't return any window + }, + + postNest: function () { + }, +}; + +ChildProcessActor.prototype.requestTypes = { + "listWorkers": ChildProcessActor.prototype.onListWorkers, +}; diff --git a/devtools/server/actors/childtab.js b/devtools/server/actors/childtab.js new file mode 100644 index 000000000..96d82e281 --- /dev/null +++ b/devtools/server/actors/childtab.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { Cr } = require("chrome"); +var { TabActor } = require("devtools/server/actors/webbrowser"); + +/** + * Tab actor for documents living in a child process. + * + * Depends on TabActor, defined in webbrowser.js. + */ + +/** + * Creates a tab actor for handling requests to the single tab, like + * attaching and detaching. ContentActor respects the actor factories + * registered with DebuggerServer.addTabActor. + * + * @param connection DebuggerServerConnection + * The conection to the client. + * @param chromeGlobal + * The content script global holding |content| and |docShell| properties for a tab. + * @param prefix + * the prefix used in protocol to create IDs for each actor. + * Used as ID identifying this particular TabActor from the parent process. + */ +function ContentActor(connection, chromeGlobal, prefix) +{ + this._chromeGlobal = chromeGlobal; + this._prefix = prefix; + TabActor.call(this, connection, chromeGlobal); + this.traits.reconfigure = false; + this._sendForm = this._sendForm.bind(this); + this._chromeGlobal.addMessageListener("debug:form", this._sendForm); + + Object.defineProperty(this, "docShell", { + value: this._chromeGlobal.docShell, + configurable: true + }); +} + +ContentActor.prototype = Object.create(TabActor.prototype); + +ContentActor.prototype.constructor = ContentActor; + +Object.defineProperty(ContentActor.prototype, "title", { + get: function () { + return this.window.document.title; + }, + enumerable: true, + configurable: true +}); + +ContentActor.prototype.exit = function () { + if (this._sendForm) { + try { + this._chromeGlobal.removeMessageListener("debug:form", this._sendForm); + } catch (e) { + if (e.result != Cr.NS_ERROR_NULL_POINTER) { + throw e; + } + // In some cases, especially when using messageManagers in non-e10s mode, we reach + // this point with a dead messageManager which only throws errors but does not + // seem to indicate in any other way that it is dead. + } + this._sendForm = null; + } + + TabActor.prototype.exit.call(this); + + this._chromeGlobal = null; +}; + +/** + * On navigation events, our URL and/or title may change, so we update our + * counterpart in the parent process that participates in the tab list. + */ +ContentActor.prototype._sendForm = function () { + this._chromeGlobal.sendAsyncMessage("debug:form", this.form()); +}; diff --git a/devtools/server/actors/chrome.js b/devtools/server/actors/chrome.js new file mode 100644 index 000000000..07cd2ad99 --- /dev/null +++ b/devtools/server/actors/chrome.js @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Ci } = require("chrome"); +const Services = require("Services"); +const { DebuggerServer } = require("../main"); +const { getChildDocShells, TabActor } = require("./webbrowser"); +const makeDebugger = require("./utils/make-debugger"); + +/** + * Creates a TabActor for debugging all the chrome content in the + * current process. Most of the implementation is inherited from TabActor. + * ChromeActor is a child of RootActor, it can be instanciated via + * RootActor.getProcess request. + * ChromeActor exposes all tab actors via its form() request, like TabActor. + * + * History lecture: + * All tab actors used to also be registered as global actors, + * so that the root actor was also exposing tab actors for the main process. + * Tab actors ended up having RootActor as parent actor, + * but more and more features of the tab actors were relying on TabActor. + * So we are now exposing a process actor that offers the same API as TabActor + * by inheriting its functionality. + * Global actors are now only the actors that are meant to be global, + * and are no longer related to any specific scope/document. + * + * @param aConnection DebuggerServerConnection + * The connection to the client. + */ +function ChromeActor(aConnection) { + TabActor.call(this, aConnection); + + // This creates a Debugger instance for chrome debugging all globals. + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: dbg => dbg.findAllGlobals(), + shouldAddNewGlobalAsDebuggee: () => true + }); + + // Ensure catching the creation of any new content docshell + this.listenForNewDocShells = true; + + // Defines the default docshell selected for the tab actor + let window = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType); + + // Default to any available top level window if there is no expected window + // (for example when we open firefox with -webide argument) + if (!window) { + window = Services.wm.getMostRecentWindow(null); + } + // On xpcshell, there is no window/docshell + let docShell = window ? window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + : null; + Object.defineProperty(this, "docShell", { + value: docShell, + configurable: true + }); +} +exports.ChromeActor = ChromeActor; + +ChromeActor.prototype = Object.create(TabActor.prototype); + +ChromeActor.prototype.constructor = ChromeActor; + +ChromeActor.prototype.isRootActor = true; + +/** + * Getter for the list of all docshells in this tabActor + * @return {Array} + */ +Object.defineProperty(ChromeActor.prototype, "docShells", { + get: function () { + // Iterate over all top-level windows and all their docshells. + let docShells = []; + let e = Services.ww.getWindowEnumerator(); + while (e.hasMoreElements()) { + let window = e.getNext(); + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + docShells = docShells.concat(getChildDocShells(docShell)); + } + + return docShells; + } +}); + +ChromeActor.prototype.observe = function (aSubject, aTopic, aData) { + TabActor.prototype.observe.call(this, aSubject, aTopic, aData); + if (!this.attached) { + return; + } + if (aTopic == "chrome-webnavigation-create") { + aSubject.QueryInterface(Ci.nsIDocShell); + this._onDocShellCreated(aSubject); + } else if (aTopic == "chrome-webnavigation-destroy") { + this._onDocShellDestroy(aSubject); + } +}; + +ChromeActor.prototype._attach = function () { + if (this.attached) { + return false; + } + + TabActor.prototype._attach.call(this); + + // Listen for any new/destroyed chrome docshell + Services.obs.addObserver(this, "chrome-webnavigation-create", false); + Services.obs.addObserver(this, "chrome-webnavigation-destroy", false); + + // Iterate over all top-level windows. + let docShells = []; + let e = Services.ww.getWindowEnumerator(); + while (e.hasMoreElements()) { + let window = e.getNext(); + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + if (docShell == this.docShell) { + continue; + } + this._progressListener.watch(docShell); + } +}; + +ChromeActor.prototype._detach = function () { + if (!this.attached) { + return false; + } + + Services.obs.removeObserver(this, "chrome-webnavigation-create"); + Services.obs.removeObserver(this, "chrome-webnavigation-destroy"); + + // Iterate over all top-level windows. + let docShells = []; + let e = Services.ww.getWindowEnumerator(); + while (e.hasMoreElements()) { + let window = e.getNext(); + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + if (docShell == this.docShell) { + continue; + } + this._progressListener.unwatch(docShell); + } + + TabActor.prototype._detach.call(this); +}; + +/* ThreadActor hooks. */ + +/** + * Prepare to enter a nested event loop by disabling debuggee events. + */ +ChromeActor.prototype.preNest = function () { + // Disable events in all open windows. + let e = Services.wm.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.suppressEventHandling(true); + windowUtils.suspendTimeouts(); + } +}; + +/** + * Prepare to exit a nested event loop by enabling debuggee events. + */ +ChromeActor.prototype.postNest = function (aNestData) { + // Enable events in all open windows. + let e = Services.wm.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.resumeTimeouts(); + windowUtils.suppressEventHandling(false); + } +}; diff --git a/devtools/server/actors/common.js b/devtools/server/actors/common.js new file mode 100644 index 000000000..0177c6749 --- /dev/null +++ b/devtools/server/actors/common.js @@ -0,0 +1,521 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const { method } = require("devtools/shared/protocol"); + +/** + * Creates "registered" actors factory meant for creating another kind of + * factories, ObservedActorFactory, during the call to listTabs. + * These factories live in DebuggerServer.{tab|global}ActorFactories. + * + * These actors only exposes: + * - `name` string attribute used to match actors by constructor name + * in DebuggerServer.remove{Global,Tab}Actor. + * - `createObservedActorFactory` function to create "observed" actors factory + * + * @param options object, function + * Either an object or a function. + * If given an object: + * + * If given a function (deprecated): + * Constructor function of an actor. + * The constructor function for this actor type. + * This expects to be called as a constructor (i.e. with 'new'), + * and passed two arguments: the DebuggerServerConnection, and + * the BrowserTabActor with which it will be associated. + * Only used for deprecated eagerly loaded actors. + * + */ +function RegisteredActorFactory(options, prefix) { + // By default the actor name will also be used for the actorID prefix. + this._prefix = prefix; + if (typeof (options) != "function") { + // actors definition registered by actorRegistryActor + if (options.constructorFun) { + this._getConstructor = () => options.constructorFun; + } else { + // Lazy actor definition, where options contains all the information + // required to load the actor lazily. + this._getConstructor = function () { + // Load the module + let mod; + try { + mod = require(options.id); + } catch (e) { + throw new Error("Unable to load actor module '" + options.id + "'.\n" + + e.message + "\n" + e.stack + "\n"); + } + // Fetch the actor constructor + let c = mod[options.constructorName]; + if (!c) { + throw new Error("Unable to find actor constructor named '" + + options.constructorName + "'. (Is it exported?)"); + } + return c; + }; + } + // Exposes `name` attribute in order to allow removeXXXActor to match + // the actor by its actor constructor name. + this.name = options.constructorName; + } else { + // Old actor case, where options is a function that is the actor constructor. + this._getConstructor = () => options; + // Exposes `name` attribute in order to allow removeXXXActor to match + // the actor by its actor constructor name. + this.name = options.name; + + // For old actors, we allow the use of a different prefix for actorID + // than for listTabs actor names, by fetching a prefix on the actor prototype. + // (Used by ChromeDebuggerActor) + if (options.prototype && options.prototype.actorPrefix) { + this._prefix = options.prototype.actorPrefix; + } + } +} +RegisteredActorFactory.prototype.createObservedActorFactory = function (conn, parentActor) { + return new ObservedActorFactory(this._getConstructor, this._prefix, conn, parentActor); +}; +exports.RegisteredActorFactory = RegisteredActorFactory; + +/** + * Creates "observed" actors factory meant for creating real actor instances. + * These factories lives in actor pools and fake various actor attributes. + * They will be replaced in actor pools by final actor instances during + * the first request for the same actorID from DebuggerServer._getOrCreateActor. + * + * ObservedActorFactory fakes the following actors attributes: + * actorPrefix (string) Used by ActorPool.addActor to compute the actor id + * actorID (string) Set by ActorPool.addActor just after being instantiated + * registeredPool (object) Set by ActorPool.addActor just after being + * instantiated + * And exposes the following method: + * createActor (function) Instantiate an actor that is going to replace + * this factory in the actor pool. + */ +function ObservedActorFactory(getConstructor, prefix, conn, parentActor) { + this._getConstructor = getConstructor; + this._conn = conn; + this._parentActor = parentActor; + + this.actorPrefix = prefix; + + this.actorID = null; + this.registeredPool = null; +} +ObservedActorFactory.prototype.createActor = function () { + // Fetch the actor constructor + let c = this._getConstructor(); + // Instantiate a new actor instance + let instance = new c(this._conn, this._parentActor); + instance.conn = this._conn; + instance.parentID = this._parentActor.actorID; + // We want the newly-constructed actor to completely replace the factory + // actor. Reusing the existing actor ID will make sure ActorPool.addActor + // does the right thing. + instance.actorID = this.actorID; + this.registeredPool.addActor(instance); + return instance; +}; +exports.ObservedActorFactory = ObservedActorFactory; + + +/** + * Methods shared between RootActor and BrowserTabActor. + */ + +/** + * Populate |this._extraActors| as specified by |aFactories|, reusing whatever + * actors are already there. Add all actors in the final extra actors table to + * |aPool|. + * + * The root actor and the tab actor use this to instantiate actors that other + * parts of the browser have specified with DebuggerServer.addTabActor and + * DebuggerServer.addGlobalActor. + * + * @param aFactories + * An object whose own property names are the names of properties to add to + * some reply packet (say, a tab actor grip or the "listTabs" response + * form), and whose own property values are actor constructor functions, as + * documented for addTabActor and addGlobalActor. + * + * @param this + * The BrowserRootActor or BrowserTabActor with which the new actors will + * be associated. It should support whatever API the |aFactories| + * constructor functions might be interested in, as it is passed to them. + * For the sake of CommonCreateExtraActors itself, it should have at least + * the following properties: + * + * - _extraActors + * An object whose own property names are factory table (and packet) + * property names, and whose values are no-argument actor constructors, + * of the sort that one can add to an ActorPool. + * + * - conn + * The DebuggerServerConnection in which the new actors will participate. + * + * - actorID + * The actor's name, for use as the new actors' parentID. + */ +exports.createExtraActors = function createExtraActors(aFactories, aPool) { + // Walk over global actors added by extensions. + for (let name in aFactories) { + let actor = this._extraActors[name]; + if (!actor) { + // Register another factory, but this time specific to this connection. + // It creates a fake actor that looks like an regular actor in the pool, + // but without actually instantiating the actor. + // It will only be instantiated on the first request made to the actor. + actor = aFactories[name].createObservedActorFactory(this.conn, this); + this._extraActors[name] = actor; + } + + // If the actor already exists in the pool, it may have been instantiated, + // so make sure not to overwrite it by a non-instantiated version. + if (!aPool.has(actor.actorID)) { + aPool.addActor(actor); + } + } +}; + +/** + * Append the extra actors in |this._extraActors|, constructed by a prior call + * to CommonCreateExtraActors, to |aObject|. + * + * @param aObject + * The object to which the extra actors should be added, under the + * property names given in the |aFactories| table passed to + * CommonCreateExtraActors. + * + * @param this + * The BrowserRootActor or BrowserTabActor whose |_extraActors| table we + * should use; see above. + */ +exports.appendExtraActors = function appendExtraActors(aObject) { + for (let name in this._extraActors) { + let actor = this._extraActors[name]; + aObject[name] = actor.actorID; + } +}; + +/** + * Construct an ActorPool. + * + * ActorPools are actorID -> actor mapping and storage. These are + * used to accumulate and quickly dispose of groups of actors that + * share a lifetime. + */ +function ActorPool(aConnection) +{ + this.conn = aConnection; + this._actors = {}; +} + +ActorPool.prototype = { + /** + * Destroy the pool. This will remove all actors from the pool. + */ + destroy: function AP_destroy() { + for (let id in this._actors) { + this.removeActor(this._actors[id]); + } + }, + + /** + * Add an actor to the pool. If the actor doesn't have an ID, allocate one + * from the connection. + * + * @param Object aActor + * The actor to be added to the pool. + */ + addActor: function AP_addActor(aActor) { + aActor.conn = this.conn; + if (!aActor.actorID) { + let prefix = aActor.actorPrefix; + if (!prefix && typeof aActor == "function") { + // typeName is a convention used with protocol.js-based actors + prefix = aActor.prototype.actorPrefix || aActor.prototype.typeName; + } + aActor.actorID = this.conn.allocID(prefix || undefined); + } + + // If the actor is already in a pool, remove it without destroying it. + if (aActor.registeredPool) { + delete aActor.registeredPool._actors[aActor.actorID]; + } + aActor.registeredPool = this; + + this._actors[aActor.actorID] = aActor; + }, + + /** + * Remove an actor from the pool. If the actor has a disconnect method, call + * it. + */ + removeActor: function AP_remove(aActor) { + delete this._actors[aActor.actorID]; + if (aActor.disconnect) { + aActor.disconnect(); + } + }, + + get: function AP_get(aActorID) { + return this._actors[aActorID] || undefined; + }, + + has: function AP_has(aActorID) { + return aActorID in this._actors; + }, + + /** + * Returns true if the pool is empty. + */ + isEmpty: function AP_isEmpty() { + return Object.keys(this._actors).length == 0; + }, + + /** + * Match the api expected by the protocol library. + */ + unmanage: function (aActor) { + return this.removeActor(aActor); + }, + + forEach: function (callback) { + for (let name in this._actors) { + callback(this._actors[name]); + } + }, +}; + +exports.ActorPool = ActorPool; + +/** + * An OriginalLocation represents a location in an original source. + * + * @param SourceActor actor + * A SourceActor representing an original source. + * @param Number line + * A line within the given source. + * @param Number column + * A column within the given line. + * @param String name + * The name of the symbol corresponding to this OriginalLocation. + */ +function OriginalLocation(actor, line, column, name) { + this._connection = actor ? actor.conn : null; + this._actorID = actor ? actor.actorID : undefined; + this._line = line; + this._column = column; + this._name = name; +} + +OriginalLocation.fromGeneratedLocation = function (generatedLocation) { + return new OriginalLocation( + generatedLocation.generatedSourceActor, + generatedLocation.generatedLine, + generatedLocation.generatedColumn + ); +}; + +OriginalLocation.prototype = { + get originalSourceActor() { + return this._connection ? this._connection.getActor(this._actorID) : null; + }, + + get originalUrl() { + let actor = this.originalSourceActor; + let source = actor.source; + return source ? source.url : actor._originalUrl; + }, + + get originalLine() { + return this._line; + }, + + get originalColumn() { + return this._column; + }, + + get originalName() { + return this._name; + }, + + get generatedSourceActor() { + throw new Error("Shouldn't access generatedSourceActor from an OriginalLocation"); + }, + + get generatedLine() { + throw new Error("Shouldn't access generatedLine from an OriginalLocation"); + }, + + get generatedColumn() { + throw new Error("Shouldn't access generatedColumn from an Originallocation"); + }, + + equals: function (other) { + return this.originalSourceActor.url == other.originalSourceActor.url && + this.originalLine === other.originalLine && + (this.originalColumn === undefined || + other.originalColumn === undefined || + this.originalColumn === other.originalColumn); + }, + + toJSON: function () { + return { + source: this.originalSourceActor.form(), + line: this.originalLine, + column: this.originalColumn + }; + } +}; + +exports.OriginalLocation = OriginalLocation; + +/** + * A GeneratedLocation represents a location in a generated source. + * + * @param SourceActor actor + * A SourceActor representing a generated source. + * @param Number line + * A line within the given source. + * @param Number column + * A column within the given line. + */ +function GeneratedLocation(actor, line, column, lastColumn) { + this._connection = actor ? actor.conn : null; + this._actorID = actor ? actor.actorID : undefined; + this._line = line; + this._column = column; + this._lastColumn = (lastColumn !== undefined) ? lastColumn : column + 1; +} + +GeneratedLocation.fromOriginalLocation = function (originalLocation) { + return new GeneratedLocation( + originalLocation.originalSourceActor, + originalLocation.originalLine, + originalLocation.originalColumn + ); +}; + +GeneratedLocation.prototype = { + get originalSourceActor() { + throw new Error(); + }, + + get originalUrl() { + throw new Error("Shouldn't access originalUrl from a GeneratedLocation"); + }, + + get originalLine() { + throw new Error("Shouldn't access originalLine from a GeneratedLocation"); + }, + + get originalColumn() { + throw new Error("Shouldn't access originalColumn from a GeneratedLocation"); + }, + + get originalName() { + throw new Error("Shouldn't access originalName from a GeneratedLocation"); + }, + + get generatedSourceActor() { + return this._connection ? this._connection.getActor(this._actorID) : null; + }, + + get generatedLine() { + return this._line; + }, + + get generatedColumn() { + return this._column; + }, + + get generatedLastColumn() { + return this._lastColumn; + }, + + equals: function (other) { + return this.generatedSourceActor.url == other.generatedSourceActor.url && + this.generatedLine === other.generatedLine && + (this.generatedColumn === undefined || + other.generatedColumn === undefined || + this.generatedColumn === other.generatedColumn); + }, + + toJSON: function () { + return { + source: this.generatedSourceActor.form(), + line: this.generatedLine, + column: this.generatedColumn, + lastColumn: this.generatedLastColumn + }; + } +}; + +exports.GeneratedLocation = GeneratedLocation; + +/** + * A method decorator that ensures the actor is in the expected state before + * proceeding. If the actor is not in the expected state, the decorated method + * returns a rejected promise. + * + * The actor's state must be at this.state property. + * + * @param String expectedState + * The expected state. + * @param String activity + * Additional info about what's going on. + * @param Function method + * The actor method to proceed with when the actor is in the expected + * state. + * + * @returns Function + * The decorated method. + */ +function expectState(expectedState, method, activity) { + return function (...args) { + if (this.state !== expectedState) { + const msg = `Wrong state while ${activity}:` + + `Expected '${expectedState}', ` + + `but current state is '${this.state}'.`; + return promise.reject(new Error(msg)); + } + + return method.apply(this, args); + }; +} + +exports.expectState = expectState; + +/** + * Proxies a call from an actor to an underlying module, stored + * as `bridge` on the actor. This allows a module to be defined in one + * place, usable by other modules/actors on the server, but a separate + * module defining the actor/RDP definition. + * + * @see Framerate implementation: devtools/server/performance/framerate.js + * @see Framerate actor definition: devtools/server/actors/framerate.js + */ +function actorBridge(methodName, definition = {}) { + return method(function () { + return this.bridge[methodName].apply(this.bridge, arguments); + }, definition); +} +exports.actorBridge = actorBridge; + +/** + * Like `actorBridge`, but without a spec definition, for when the actor is + * created with `ActorClassWithSpec` rather than vanilla `ActorClass`. + */ +function actorBridgeWithSpec (methodName) { + return method(function () { + return this.bridge[methodName].apply(this.bridge, arguments); + }); +} +exports.actorBridgeWithSpec = actorBridgeWithSpec; diff --git a/devtools/server/actors/css-properties.js b/devtools/server/actors/css-properties.js new file mode 100644 index 000000000..d24c133d4 --- /dev/null +++ b/devtools/server/actors/css-properties.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); + +loader.lazyGetter(this, "DOMUtils", () => { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); + +const protocol = require("devtools/shared/protocol"); +const { ActorClassWithSpec, Actor } = protocol; +const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties"); +const { CSS_PROPERTIES, CSS_TYPES } = require("devtools/shared/css/properties-db"); +const { cssColors } = require("devtools/shared/css/color-db"); + +exports.CssPropertiesActor = ActorClassWithSpec(cssPropertiesSpec, { + typeName: "cssProperties", + + initialize(conn, parent) { + Actor.prototype.initialize.call(this, conn); + this.parent = parent; + }, + + destroy() { + Actor.prototype.destroy.call(this); + }, + + getCSSDatabase() { + const properties = generateCssProperties(); + const pseudoElements = DOMUtils.getCSSPseudoElementNames(); + + return { properties, pseudoElements }; + } +}); + +/** + * Generate the CSS properties object. Every key is the property name, while + * the values are objects that contain information about that property. + * + * @return {Object} + */ +function generateCssProperties() { + const properties = {}; + const propertyNames = DOMUtils.getCSSPropertyNames(DOMUtils.INCLUDE_ALIASES); + const colors = Object.keys(cssColors); + + propertyNames.forEach(name => { + // Get the list of CSS types this property supports. + let supports = []; + for (let type in CSS_TYPES) { + if (safeCssPropertySupportsType(name, DOMUtils["TYPE_" + type])) { + supports.push(CSS_TYPES[type]); + } + } + + // Don't send colors over RDP, these will be re-attached by the front. + let values = DOMUtils.getCSSValuesForProperty(name); + if (values.includes("aliceblue")) { + values = values.filter(x => !colors.includes(x)); + values.unshift("COLOR"); + } + + let subproperties = DOMUtils.getSubpropertiesForCSSProperty(name); + + // In order to maintain any backwards compatible changes when debugging older + // clients, take the definition from the static CSS properties database, and fill it + // in with the most recent property definition from the server. + const clientDefinition = CSS_PROPERTIES[name] || {}; + const serverDefinition = { + isInherited: DOMUtils.isInheritedProperty(name), + values, + supports, + subproperties, + }; + properties[name] = Object.assign(clientDefinition, serverDefinition); + }); + + return properties; +} +exports.generateCssProperties = generateCssProperties; + +/** + * Test if a CSS is property is known using server-code. + * + * @param {string} name + * @return {Boolean} + */ +function isCssPropertyKnown(name) { + try { + // If the property name is unknown, the cssPropertyIsShorthand + // will throw an exception. But if it is known, no exception will + // be thrown; so we just ignore the return value. + DOMUtils.cssPropertyIsShorthand(name); + return true; + } catch (e) { + return false; + } +} + +exports.isCssPropertyKnown = isCssPropertyKnown; + +/** + * A wrapper for DOMUtils.cssPropertySupportsType that ignores invalid + * properties. + * + * @param {String} name The property name. + * @param {number} type The type tested for support. + * @return {Boolean} Whether the property supports the type. + * If the property is unknown, false is returned. + */ +function safeCssPropertySupportsType(name, type) { + try { + return DOMUtils.cssPropertySupportsType(name, type); + } catch (e) { + return false; + } +} diff --git a/devtools/server/actors/csscoverage.js b/devtools/server/actors/csscoverage.js new file mode 100644 index 000000000..2f700656f --- /dev/null +++ b/devtools/server/actors/csscoverage.js @@ -0,0 +1,726 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci } = require("chrome"); + +const Services = require("Services"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); + +const events = require("sdk/event/core"); +const protocol = require("devtools/shared/protocol"); +const { cssUsageSpec } = require("devtools/shared/specs/csscoverage"); + +loader.lazyGetter(this, "DOMUtils", () => { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); +loader.lazyRequireGetter(this, "stylesheets", "devtools/server/actors/stylesheets"); +loader.lazyRequireGetter(this, "prettifyCSS", "devtools/shared/inspector/css-logic", true); + +const CSSRule = Ci.nsIDOMCSSRule; + +const MAX_UNUSED_RULES = 10000; + +/** + * Allow: let foo = l10n.lookup("csscoverageFoo"); + */ +const l10n = exports.l10n = { + _URI: "chrome://devtools-shared/locale/csscoverage.properties", + lookup: function (msg) { + if (this._stringBundle == null) { + this._stringBundle = Services.strings.createBundle(this._URI); + } + return this._stringBundle.GetStringFromName(msg); + } +}; + +/** + * CSSUsage manages the collection of CSS usage data. + * The core of a CSSUsage is a JSON-able data structure called _knownRules + * which looks like this: + * This records the CSSStyleRules and their usage. + * The format is: + * Map({ + * ||: { + * selectorText: , + * test: , + * cssText: , + * isUsed: , + * presentOn: Set([ , ... ]), + * preLoadOn: Set([ , ... ]), + * isError: , + * } + * }) + * + * For example: + * this._knownRules = Map({ + * "http://eg.com/styles1.css|15|0": { + * selectorText: "p.quote:hover", + * test: "p.quote", + * cssText: "p.quote { color: red; }", + * isUsed: true, + * presentOn: Set([ "http://eg.com/page1.html", ... ]), + * preLoadOn: Set([ "http://eg.com/page1.html" ]), + * isError: false, + * }, ... + * }); + */ +var CSSUsageActor = protocol.ActorClassWithSpec(cssUsageSpec, { + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, conn); + + this._tabActor = tabActor; + this._running = false; + + this._onTabLoad = this._onTabLoad.bind(this); + this._onChange = this._onChange.bind(this); + + this._notifyOn = Ci.nsIWebProgress.NOTIFY_STATUS | + Ci.nsIWebProgress.NOTIFY_STATE_ALL; + }, + + destroy: function () { + this._tabActor = undefined; + + delete this._onTabLoad; + delete this._onChange; + + protocol.Actor.prototype.destroy.call(this); + }, + + /** + * Begin recording usage data + * @param noreload It's best if we start by reloading the current page + * because that starts the test at a known point, but there could be reasons + * why we don't want to do that (e.g. the page contains state that will be + * lost across a reload) + */ + start: function (noreload) { + if (this._running) { + throw new Error(l10n.lookup("csscoverageRunningError")); + } + + this._isOneShot = false; + this._visitedPages = new Set(); + this._knownRules = new Map(); + this._running = true; + this._tooManyUnused = false; + + this._progressListener = { + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference ]), + + onStateChange: (progress, request, flags, status) => { + let isStop = flags & Ci.nsIWebProgressListener.STATE_STOP; + let isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + + if (isStop && isWindow) { + this._onTabLoad(progress.DOMWindow.document); + } + }, + + onLocationChange: () => {}, + onProgressChange: () => {}, + onSecurityChange: () => {}, + onStatusChange: () => {}, + destroy: () => {} + }; + + this._progress = this._tabActor.docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + this._progress.addProgressListener(this._progressListener, this._notifyOn); + + if (noreload) { + // If we're not starting by reloading the page, then pretend that onload + // has just happened. + this._onTabLoad(this._tabActor.window.document); + } else { + this._tabActor.window.location.reload(); + } + + events.emit(this, "state-change", { isRunning: true }); + }, + + /** + * Cease recording usage data + */ + stop: function () { + if (!this._running) { + throw new Error(l10n.lookup("csscoverageNotRunningError")); + } + + this._progress.removeProgressListener(this._progressListener, this._notifyOn); + this._progress = undefined; + + this._running = false; + events.emit(this, "state-change", { isRunning: false }); + }, + + /** + * Start/stop recording usage data depending on what we're currently doing. + */ + toggle: function () { + return this._running ? this.stop() : this.start(); + }, + + /** + * Running start() quickly followed by stop() does a bunch of unnecessary + * work, so this cuts all that out + */ + oneshot: function () { + if (this._running) { + throw new Error(l10n.lookup("csscoverageRunningError")); + } + + this._isOneShot = true; + this._visitedPages = new Set(); + this._knownRules = new Map(); + + this._populateKnownRules(this._tabActor.window.document); + this._updateUsage(this._tabActor.window.document, false); + }, + + /** + * Called by the ProgressListener to simulate a "load" event + */ + _onTabLoad: function (document) { + this._populateKnownRules(document); + this._updateUsage(document, true); + + this._observeMutations(document); + }, + + /** + * Setup a MutationObserver on the current document + */ + _observeMutations: function (document) { + let MutationObserver = document.defaultView.MutationObserver; + let observer = new MutationObserver(mutations => { + // It's possible that one of the mutations in this list adds a 'use' of + // a CSS rule, and another takes it away. See Bug 1010189 + this._onChange(document); + }); + + observer.observe(document, { + attributes: true, + childList: true, + characterData: false, + subtree: true + }); + }, + + /** + * Event handler for whenever we think the page has changed in a way that + * means the CSS usage might have changed. + */ + _onChange: function (document) { + // Ignore changes pre 'load' + if (!this._visitedPages.has(getURL(document))) { + return; + } + this._updateUsage(document, false); + }, + + /** + * Called whenever we think the list of stylesheets might have changed so + * we can update the list of rules that we should be checking + */ + _populateKnownRules: function (document) { + let url = getURL(document); + this._visitedPages.add(url); + // Go through all the rules in the current sheets adding them to knownRules + // if needed and adding the current url to the list of pages they're on + for (let rule of getAllSelectorRules(document)) { + let ruleId = ruleToId(rule); + let ruleData = this._knownRules.get(ruleId); + if (ruleData == null) { + ruleData = { + selectorText: rule.selectorText, + cssText: rule.cssText, + test: getTestSelector(rule.selectorText), + isUsed: false, + presentOn: new Set(), + preLoadOn: new Set(), + isError: false + }; + this._knownRules.set(ruleId, ruleData); + } + + ruleData.presentOn.add(url); + } + }, + + /** + * Update knownRules with usage information from the current page + */ + _updateUsage: function (document, isLoad) { + let qsaCount = 0; + + // Update this._data with matches to say 'used at load time' by sheet X + let url = getURL(document); + + for (let [ , ruleData ] of this._knownRules) { + // If it broke before, don't try again selectors don't change + if (ruleData.isError) { + continue; + } + + // If it's used somewhere already, don't bother checking again unless + // this is a load event in which case we need to add preLoadOn + if (!isLoad && ruleData.isUsed) { + continue; + } + + // Ignore rules that are not present on this page + if (!ruleData.presentOn.has(url)) { + continue; + } + + qsaCount++; + if (qsaCount > MAX_UNUSED_RULES) { + console.error("Too many unused rules on " + url + " "); + this._tooManyUnused = true; + continue; + } + + try { + let match = document.querySelector(ruleData.test); + if (match != null) { + ruleData.isUsed = true; + if (isLoad) { + ruleData.preLoadOn.add(url); + } + } + } catch (ex) { + ruleData.isError = true; + } + } + }, + + /** + * Returns a JSONable structure designed to help marking up the style editor, + * which describes the CSS selector usage. + * Example: + * [ + * { + * selectorText: "p#content", + * usage: "unused|used", + * start: { line: 3, column: 0 }, + * }, + * ... + * ] + */ + createEditorReport: function (url) { + if (this._knownRules == null) { + return { reports: [] }; + } + + let reports = []; + for (let [ruleId, ruleData] of this._knownRules) { + let { url: ruleUrl, line, column } = deconstructRuleId(ruleId); + if (ruleUrl !== url || ruleData.isUsed) { + continue; + } + + let ruleReport = { + selectorText: ruleData.selectorText, + start: { line: line, column: column } + }; + + if (ruleData.end) { + ruleReport.end = ruleData.end; + } + + reports.push(ruleReport); + } + + return { reports: reports }; + }, + + /** + * Compute the stylesheet URL and delegate the report creation to createEditorReport. + * See createEditorReport documentation. + * + * @param {StyleSheetActor} stylesheetActor + * the stylesheet actor for which the coverage report should be generated. + */ + createEditorReportForSheet: function (stylesheetActor) { + let url = sheetToUrl(stylesheetActor.rawSheet); + return this.createEditorReport(url); + }, + + /** + * Returns a JSONable structure designed for the page report which shows + * the recommended changes to a page. + * + * "preload" means that a rule is used before the load event happens, which + * means that the page could by optimized by placing it in a +
+
+
+
+
+
+
+
+
+
+
+ diff --git a/devtools/server/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini new file mode 100644 index 000000000..c05933230 --- /dev/null +++ b/devtools/server/tests/browser/browser.ini @@ -0,0 +1,97 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + animation.html + doc_allocations.html + doc_force_cc.html + doc_force_gc.html + doc_innerHTML.html + doc_perf.html + navigate-first.html + navigate-second.html + storage-dynamic-windows.html + storage-listings.html + storage-unsecured-iframe.html + storage-updates.html + storage-secured-iframe.html + stylesheets-nested-iframes.html + timeline-iframe-child.html + timeline-iframe-parent.html + director-script-target.html + storage-helpers.js + !/devtools/server/tests/mochitest/hello-actor.js + +[browser_animation_emitMutations.js] +[browser_animation_getFrames.js] +[browser_animation_getProperties.js] +[browser_animation_getMultipleStates.js] +[browser_animation_getPlayers.js] +[browser_animation_getStateAfterFinished.js] +[browser_animation_getSubTreeAnimations.js] +[browser_animation_keepFinished.js] +[browser_animation_playerState.js] +[browser_animation_playPauseIframe.js] +[browser_animation_playPauseSeveral.js] +[browser_animation_reconstructState.js] +[browser_animation_refreshTransitions.js] +[browser_animation_setCurrentTime.js] +[browser_animation_setPlaybackRate.js] +[browser_animation_simple.js] +[browser_animation_updatedState.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_01.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_02.js] +[browser_canvasframe_helper_03.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_04.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_05.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_06.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_markers-cycle-collection.js] +[browser_markers-docloading-01.js] +[browser_markers-docloading-02.js] +[browser_markers-docloading-03.js] +[browser_markers-gc.js] +[browser_markers-minor-gc.js] +[browser_markers-parse-html.js] +[browser_markers-styles.js] +[browser_markers-timestamp.js] +[browser_navigateEvents.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-allocation-data.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-profiler-01.js] +[browser_perf-profiler-02.js] +skip-if = true # Needs to be updated for async actor destruction +[browser_perf-profiler-03.js] +skip-if = true # Needs to be updated for async actor destruction +[browser_perf-realtime-markers.js] +[browser_perf-recording-actor-01.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-recording-actor-02.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-samples-01.js] +[browser_perf-samples-02.js] +#[browser_perf-front-profiler-01.js] bug 1077464 +#[browser_perf-front-profiler-05.js] bug 1077464 +#[browser_perf-front-profiler-06.js] +[browser_storage_dynamic_windows.js] +[browser_storage_listings.js] +[browser_storage_updates.js] +[browser_stylesheets_getTextEmpty.js] +[browser_stylesheets_nested-iframes.js] +[browser_timeline.js] +[browser_timeline_actors.js] +[browser_timeline_iframes.js] +[browser_directorscript_actors_exports.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_directorscript_actors_error_events.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_directorscript_actors.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_register_actor.js] diff --git a/devtools/server/tests/browser/browser_animation_emitMutations.js b/devtools/server/tests/browser/browser_animation_emitMutations.js new file mode 100644 index 000000000..4783a8209 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_emitMutations.js @@ -0,0 +1,62 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the AnimationsActor emits events about changed animations on a +// node after getAnimationPlayersForNode was called on that node. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve a non-animated node"); + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + + info("Retrieve the animation player for the node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The node has no animation players"); + + info("Listen for new animations"); + let onMutations = once(animations, "mutations"); + + info("Add a couple of animation on the node"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "multiple-animations"} + ]); + let changes = yield onMutations; + + ok(true, "The mutations event was emitted"); + is(changes.length, 2, "There are 2 changes in the mutation event"); + ok(changes.every(({type}) => type === "added"), "Both changes are additions"); + + let names = changes.map(c => c.player.initialState.name).sort(); + is(names[0], "glow", "The animation 'glow' was added"); + is(names[1], "move", "The animation 'move' was added"); + + info("Store the 2 new players for comparing later"); + let p1 = changes[0].player; + let p2 = changes[1].player; + + info("Listen for removed animations"); + onMutations = once(animations, "mutations"); + + info("Remove the animation css class on the node"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "not-animated"} + ]); + + changes = yield onMutations; + + ok(true, "The mutations event was emitted"); + is(changes.length, 2, "There are 2 changes in the mutation event"); + ok(changes.every(({type}) => type === "removed"), "Both are removals"); + ok(changes[0].player === p1 || changes[0].player === p2, + "The first removed player was one of the previously added players"); + ok(changes[1].player === p1 || changes[1].player === p2, + "The second removed player was one of the previously added players"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_getFrames.js b/devtools/server/tests/browser/browser_animation_getFrames.js new file mode 100644 index 000000000..25ccfae3b --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getFrames.js @@ -0,0 +1,32 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationPlayerActor exposes a getFrames method that returns +// the list of keyframes in the animation. + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Get the test node and its animation front"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.getFrames, "The front has the getFrames method"); + + let frames = yield player.getFrames(); + is(frames.length, 2, "The correct number of keyframes was retrieved"); + ok(frames[0].transform, "Frame 0 has the transform property"); + ok(frames[1].transform, "Frame 1 has the transform property"); + // Note that we don't really test the content of the frame object here on + // purpose. This object comes straight out of the web animations API + // unmodified. + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_getMultipleStates.js b/devtools/server/tests/browser/browser_animation_getMultipleStates.js new file mode 100644 index 000000000..4436695b0 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js @@ -0,0 +1,55 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* setup() { + yield SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]] + }); +}); + +// Check that the duration, iterationCount and delay are retrieved correctly for +// multiple animations. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playerHasAnInitialState(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playerHasAnInitialState(walker, animations) { + let state = yield getAnimationStateForNode(walker, animations, + ".delayed-multiple-animations", 0); + + ok(state.duration, 50000, + "The duration of the first animation is correct"); + ok(state.iterationCount, 10, + "The iterationCount of the first animation is correct"); + ok(state.delay, 1000, + "The delay of the first animation is correct"); + + state = yield getAnimationStateForNode(walker, animations, + ".delayed-multiple-animations", 1); + + ok(state.duration, 100000, + "The duration of the secon animation is correct"); + ok(state.iterationCount, 30, + "The iterationCount of the secon animation is correct"); + ok(state.delay, 750, + "The delay of the secon animation is correct"); +} + +function* getAnimationStateForNode(walker, animations, selector, playerIndex) { + let node = yield walker.querySelector(walker.rootNode, selector); + let players = yield animations.getAnimationPlayersForNode(node); + let player = players[playerIndex]; + yield player.ready(); + let state = yield player.getCurrentState(); + return state; +} diff --git a/devtools/server/tests/browser/browser_animation_getPlayers.js b/devtools/server/tests/browser/browser_animation_getPlayers.js new file mode 100644 index 000000000..a99a4dc4e --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getPlayers.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check the output of getAnimationPlayersForNode + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield theRightNumberOfPlayersIsReturned(walker, animations); + yield playersCanBePausedAndResumed(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* theRightNumberOfPlayersIsReturned(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, + "0 players were returned for the unanimated node"); + + node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 1, + "One animation player was returned"); + + node = yield walker.querySelector(walker.rootNode, ".multiple-animations"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 2, + "Two animation players were returned"); + + node = yield walker.querySelector(walker.rootNode, ".transition"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 1, + "One animation player was returned for the transitioned node"); +} + +function* playersCanBePausedAndResumed(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + yield player.ready(); + + ok(player.initialState, + "The player has an initialState"); + ok(player.getCurrentState, + "The player has the getCurrentState method"); + is(player.initialState.playState, "running", + "The animation is currently running"); + + yield player.pause(); + let state = yield player.getCurrentState(); + is(state.playState, "paused", + "The animation is now paused"); + + yield player.play(); + state = yield player.getCurrentState(); + is(state.playState, "running", + "The animation is now running again"); +} diff --git a/devtools/server/tests/browser/browser_animation_getProperties.js b/devtools/server/tests/browser/browser_animation_getProperties.js new file mode 100644 index 000000000..e95db544c --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getProperties.js @@ -0,0 +1,36 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationPlayerActor exposes a getProperties method that +// returns the list of animated properties in the animation. + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function* () { + let {client, walker, animations} = yield initAnimationsFrontForUrl(URL); + + info("Get the test node and its animation front"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.getProperties, "The front has the getProperties method"); + + let properties = yield player.getProperties(); + is(properties.length, 1, "The correct number of properties was retrieved"); + + let propertyObject = properties[0]; + is(propertyObject.name, "transform", "Property 0 is transform"); + + is(propertyObject.values.length, 2, + "The correct number of property values was retrieved"); + + // Note that we don't really test the content of the frame object here on + // purpose. This object comes straight out of the web animations API + // unmodified. + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js new file mode 100644 index 000000000..dd33237c1 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js @@ -0,0 +1,55 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the right duration/iterationCount/delay are retrieved even when +// the node has multiple animations and one of them already ended before getting +// the player objects. +// See devtools/server/actors/animation.js |getPlayerIndex| for more +// information. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve a non animated node"); + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + + info("Apply the multiple-animations-2 class to start the animations"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "multiple-animations-2"} + ]); + + info("Get the list of players, by the time this executes, the first, " + + "short, animation should have ended."); + let players = yield animations.getAnimationPlayersForNode(node); + if (players.length === 3) { + info("The short animation hasn't ended yet, wait for a bit."); + // The animation lasts for 500ms, so 1000ms should do it. + yield new Promise(resolve => setTimeout(resolve, 1000)); + + info("And get the list again"); + players = yield animations.getAnimationPlayersForNode(node); + } + + is(players.length, 2, "2 animations remain on the node"); + + is(players[0].state.duration, 100000, + "The duration of the first animation is correct"); + is(players[0].state.delay, 2000, + "The delay of the first animation is correct"); + is(players[0].state.iterationCount, null, + "The iterationCount of the first animation is correct"); + + is(players[1].state.duration, 300000, + "The duration of the second animation is correct"); + is(players[1].state.delay, 1000, + "The delay of the second animation is correct"); + is(players[1].state.iterationCount, 100, + "The iterationCount of the second animation is correct"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js new file mode 100644 index 000000000..50782d6de --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js @@ -0,0 +1,38 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationsActor can retrieve all animations inside a node's +// subtree (but not going into iframes). + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function* () { + info("Creating a test document with 2 iframes containing animated nodes"); + + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8," + + ""); + + info("Try retrieving all animations from the root doc's node"); + let rootBody = yield walker.querySelector(walker.rootNode, "body"); + let players = yield animations.getAnimationPlayersForNode(rootBody); + is(players.length, 0, "The node has no animation players"); + + info("Retrieve all animations from the iframe's node"); + let iframe = yield walker.querySelector(walker.rootNode, "#iframe"); + let {nodes} = yield walker.children(iframe); + let frameBody = yield walker.querySelector(nodes[0], "body"); + players = yield animations.getAnimationPlayersForNode(frameBody); + + // Testing for a hard-coded number of animations here would intermittently + // fail depending on how fast or slow the test is (indeed, the test page + // contains short transitions, and delayed animations). So just make sure we + // at least have the infinitely running animations. + ok(players.length >= 4, "All subtree animations were retrieved"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_keepFinished.js b/devtools/server/tests/browser/browser_animation_keepFinished.js new file mode 100644 index 000000000..a3240a5e0 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_keepFinished.js @@ -0,0 +1,54 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the AnimationsActor doesn't report finished animations as removed. +// Indeed, animations that only have the "finished" playState can be modified +// still, so we want the AnimationsActor to preserve the corresponding +// AnimationPlayerActor. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve a non-animated node"); + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + + info("Retrieve the animation player for the node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The node has no animation players"); + + info("Listen for new animations"); + let reportedMutations = []; + function onMutations(mutations) { + reportedMutations = [...reportedMutations, ...mutations]; + } + animations.on("mutations", onMutations); + + info("Add a short animation on the node"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "short-animation"} + ]); + + info("Wait for longer than the animation's duration"); + yield wait(2000); + + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The added animation is surely finished"); + + is(reportedMutations.length, 1, "Only one mutation was reported"); + is(reportedMutations[0].type, "added", "The mutation was an addition"); + + animations.off("mutations", onMutations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} diff --git a/devtools/server/tests/browser/browser_animation_playPauseIframe.js b/devtools/server/tests/browser/browser_animation_playPauseIframe.js new file mode 100644 index 000000000..52320b84e --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js @@ -0,0 +1,51 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationsActor can pause/play all animations even those +// within iframes. + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function* () { + info("Creating a test document with 2 iframes containing animated nodes"); + + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8," + + "" + + ""); + + info("Getting the 2 iframe container nodes and animated nodes in them"); + let nodeInFrame1 = yield getNodeInFrame(walker, "#i1", ".simple-animation"); + let nodeInFrame2 = yield getNodeInFrame(walker, "#i2", ".simple-animation"); + + info("Pause all animations in the test document"); + yield animations.pauseAll(); + yield checkState(animations, nodeInFrame1, "paused"); + yield checkState(animations, nodeInFrame2, "paused"); + + info("Play all animations in the test document"); + yield animations.playAll(); + yield checkState(animations, nodeInFrame1, "running"); + yield checkState(animations, nodeInFrame2, "running"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* checkState(animations, nodeFront, playState) { + info("Getting the AnimationPlayerFront for the test node"); + let [player] = yield animations.getAnimationPlayersForNode(nodeFront); + yield player.ready; + let state = yield player.getCurrentState(); + is(state.playState, playState, + "The playState of the test node is " + playState); +} + +function* getNodeInFrame(walker, frameSelector, nodeSelector) { + let iframe = yield walker.querySelector(walker.rootNode, frameSelector); + let {nodes} = yield walker.children(iframe); + return walker.querySelector(nodes[0], nodeSelector); +} diff --git a/devtools/server/tests/browser/browser_animation_playPauseSeveral.js b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js new file mode 100644 index 000000000..9c52b5f57 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js @@ -0,0 +1,92 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationsActor can pause/play all animations at once, and +// check that it can also pause/play a given list of animations at once. + +// List of selectors that match "all" animated nodes in the test page. +// This list misses a bunch of animated nodes on purpose. Only the ones that +// have infinite animations are listed. This is done to avoid intermittents +// caused when finite animations are already done playing by the time the test +// runs. +const ALL_ANIMATED_NODES = [".simple-animation", ".multiple-animations", + ".delayed-animation"]; + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Pause all animations in the test document"); + yield animations.pauseAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused"); + + info("Play all animations in the test document"); + yield animations.playAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); + + info("Pause all animations in the test document using toggleAll"); + yield animations.toggleAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused"); + + info("Play all animations in the test document using toggleAll"); + yield animations.toggleAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); + + info("Play all animations from multiple animated node using toggleSeveral"); + let players = yield getPlayersFor(walker, animations, + [".multiple-animations"]); + is(players.length, 2, "Node has 2 animation players"); + yield animations.toggleSeveral(players, false); + let state1 = yield players[0].getCurrentState(); + is(state1.playState, "running", + "The playState of the first player is running"); + let state2 = yield players[1].getCurrentState(); + is(state2.playState, "running", + "The playState of the second player is running"); + + info("Pause one animation from a multiple animated node using toggleSeveral"); + yield animations.toggleSeveral([players[0]], true); + state1 = yield players[0].getCurrentState(); + is(state1.playState, "paused", "The playState of the first player is paused"); + state2 = yield players[1].getCurrentState(); + is(state2.playState, "running", + "The playState of the second player is running"); + + info("Play the same animation"); + yield animations.toggleSeveral([players[0]], false); + state1 = yield players[0].getCurrentState(); + is(state1.playState, "running", + "The playState of the first player is running"); + state2 = yield players[1].getCurrentState(); + is(state2.playState, "running", + "The playState of the second player is running"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* checkStates(walker, animations, selectors, playState) { + info("Checking the playState of all the nodes that have infinite running " + + "animations"); + + for (let selector of selectors) { + info("Getting the AnimationPlayerFront for node " + selector); + let [player] = yield getPlayersFor(walker, animations, selector); + yield player.ready(); + yield checkPlayState(player, selector, playState); + } +} + +function* getPlayersFor(walker, animations, selector) { + let node = yield walker.querySelector(walker.rootNode, selector); + return animations.getAnimationPlayersForNode(node); +} + +function* checkPlayState(player, selector, expectedState) { + let state = yield player.getCurrentState(); + is(state.playState, expectedState, + "The playState of node " + selector + " is " + expectedState); +} diff --git a/devtools/server/tests/browser/browser_animation_playerState.js b/devtools/server/tests/browser/browser_animation_playerState.js new file mode 100644 index 000000000..ac5842e39 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_playerState.js @@ -0,0 +1,123 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check the animation player's initial state + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playerHasAnInitialState(walker, animations); + yield playerStateIsCorrect(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playerHasAnInitialState(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.initialState, "The player front has an initial state"); + ok("startTime" in player.initialState, "Player's state has startTime"); + ok("currentTime" in player.initialState, "Player's state has currentTime"); + ok("playState" in player.initialState, "Player's state has playState"); + ok("playbackRate" in player.initialState, "Player's state has playbackRate"); + ok("name" in player.initialState, "Player's state has name"); + ok("duration" in player.initialState, "Player's state has duration"); + ok("delay" in player.initialState, "Player's state has delay"); + ok("iterationCount" in player.initialState, + "Player's state has iterationCount"); + ok("fill" in player.initialState, "Player's state has fill"); + ok("easing" in player.initialState, "Player's state has easing"); + ok("direction" in player.initialState, "Player's state has direction"); + ok("isRunningOnCompositor" in player.initialState, + "Player's state has isRunningOnCompositor"); + ok("type" in player.initialState, "Player's state has type"); + ok("documentCurrentTime" in player.initialState, + "Player's state has documentCurrentTime"); +} + +function* playerStateIsCorrect(walker, animations) { + info("Checking the state of the simple animation"); + + let player = yield getAnimationPlayerForNode(walker, animations, + ".simple-animation", 0); + let state = yield player.getCurrentState(); + is(state.name, "move", "Name is correct"); + is(state.duration, 200000, "Duration is correct"); + // null = infinite count + is(state.iterationCount, null, "Iteration count is correct"); + is(state.fill, "none", "Fill is correct"); + is(state.easing, "linear", "Easing is correct"); + is(state.direction, "normal", "Direction is correct"); + is(state.playState, "running", "PlayState is correct"); + is(state.playbackRate, 1, "PlaybackRate is correct"); + is(state.type, "cssanimation", "Type is correct"); + + info("Checking the state of the transition"); + + player = + yield getAnimationPlayerForNode(walker, animations, ".transition", 0); + state = yield player.getCurrentState(); + is(state.name, "width", "Transition name matches transition property"); + is(state.duration, 500000, "Transition duration is correct"); + // transitions run only once + is(state.iterationCount, 1, "Transition iteration count is correct"); + is(state.fill, "backwards", "Transition fill is correct"); + is(state.easing, "linear", "Transition easing is correct"); + is(state.direction, "normal", "Transition direction is correct"); + is(state.playState, "running", "Transition playState is correct"); + is(state.playbackRate, 1, "Transition playbackRate is correct"); + is(state.type, "csstransition", "Transition type is correct"); + // chech easing in keyframe + let keyframes = yield player.getFrames(); + is(keyframes.length, 2, "Transition length of keyframe is correct"); + is(keyframes[0].easing, + "ease-out", "Transition kerframes's easing is correct"); + + info("Checking the state of one of multiple animations on a node"); + + // Checking the 2nd player + player = yield getAnimationPlayerForNode(walker, animations, + ".multiple-animations", 1); + state = yield player.getCurrentState(); + is(state.name, "glow", "The 2nd animation's name is correct"); + is(state.duration, 100000, "The 2nd animation's duration is correct"); + is(state.iterationCount, 5, "The 2nd animation's iteration count is correct"); + is(state.fill, "both", "The 2nd animation's fill is correct"); + is(state.easing, "linear", "The 2nd animation's easing is correct"); + is(state.direction, "reverse", "The 2nd animation's direction is correct"); + is(state.playState, "running", "The 2nd animation's playState is correct"); + is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct"); + // chech easing in keyframe + keyframes = yield player.getFrames(); + is(keyframes.length, 2, "The 2nd animation's length of keyframe is correct"); + is(keyframes[0].easing, + "ease-out", "The 2nd animation's easing of kerframes is correct"); + + info("Checking the state of an animation with delay"); + + player = yield getAnimationPlayerForNode(walker, animations, + ".delayed-animation", 0); + state = yield player.getCurrentState(); + is(state.delay, 5000, "The animation delay is correct"); + + info("Checking the state of an transition with delay"); + + player = yield getAnimationPlayerForNode(walker, animations, + ".delayed-transition", 0); + state = yield player.getCurrentState(); + is(state.delay, 3000, "The transition delay is correct"); +} + +function* getAnimationPlayerForNode(walker, animations, nodeSelector, index) { + let node = yield walker.querySelector(walker.rootNode, nodeSelector); + let players = yield animations.getAnimationPlayersForNode(node); + let player = players[index]; + yield player.ready(); + return player; +} diff --git a/devtools/server/tests/browser/browser_animation_reconstructState.js b/devtools/server/tests/browser/browser_animation_reconstructState.js new file mode 100644 index 000000000..cd3007b86 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_reconstructState.js @@ -0,0 +1,38 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that, even though the AnimationPlayerActor only sends the bits of its +// state that change, the front reconstructs the whole state everytime. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playerHasCompleteStateAtAllTimes(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playerHasCompleteStateAtAllTimes(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + yield player.ready(); + + // Get the list of state key names from the initialstate. + let keys = Object.keys(player.initialState); + + // Get the state over and over again and check that the object returned + // contains all keys. + // Normally, only the currentTime will have changed in between 2 calls. + for (let i = 0; i < 10; i++) { + yield player.refreshState(); + keys.forEach(key => { + ok(typeof player.state[key] !== "undefined", + "The state retrieved has key " + key); + }); + } +} diff --git a/devtools/server/tests/browser/browser_animation_refreshTransitions.js b/devtools/server/tests/browser/browser_animation_refreshTransitions.js new file mode 100644 index 000000000..4cec0df69 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js @@ -0,0 +1,77 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// When a transition finishes, no "removed" event is sent because it may still +// be used, but when it restarts again (transitions back), then a new +// AnimationPlayerFront should be sent, and the old one should be removed. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve the test node"); + let node = yield walker.querySelector(walker.rootNode, ".all-transitions"); + + info("Retrieve the animation players for the node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The node has no animation players yet"); + + info("Play a transition by adding the expand class, wait for mutations"); + let onMutations = expectMutationEvents(animations, 2); + let cpow = content.document.querySelector(".all-transitions"); + cpow.classList.add("expand"); + let reportedMutations = yield onMutations; + + is(reportedMutations.length, 2, "2 mutation events were received"); + is(reportedMutations[0].type, "added", "The first event was 'added'"); + is(reportedMutations[1].type, "added", "The second event was 'added'"); + + info("Wait for the transitions to be finished"); + yield waitForEnd(reportedMutations[0].player); + yield waitForEnd(reportedMutations[1].player); + + info("Play the transition back by removing the class, wait for mutations"); + onMutations = expectMutationEvents(animations, 4); + cpow.classList.remove("expand"); + reportedMutations = yield onMutations; + + is(reportedMutations.length, 4, "4 new mutation events were received"); + is(reportedMutations.filter(m => m.type === "removed").length, 2, + "2 'removed' events were sent (for the old transitions)"); + is(reportedMutations.filter(m => m.type === "added").length, 2, + "2 'added' events were sent (for the new transitions)"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function expectMutationEvents(animationsFront, nbOfEvents) { + return new Promise(resolve => { + let reportedMutations = []; + function onMutations(mutations) { + reportedMutations = [...reportedMutations, ...mutations]; + info("Received " + reportedMutations.length + " mutation events, " + + "expecting " + nbOfEvents); + if (reportedMutations.length === nbOfEvents) { + animationsFront.off("mutations", onMutations); + resolve(reportedMutations); + } + } + + info("Start listening for mutation events from the AnimationsFront"); + animationsFront.on("mutations", onMutations); + }); +} + +function* waitForEnd(animationFront) { + let playState; + while (playState !== "finished") { + let state = yield animationFront.getCurrentState(); + playState = state.playState; + info("Wait for transition " + animationFront.state.name + + " to finish, playState=" + playState); + } +} diff --git a/devtools/server/tests/browser/browser_animation_setCurrentTime.js b/devtools/server/tests/browser/browser_animation_setCurrentTime.js new file mode 100644 index 000000000..16dbaa544 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js @@ -0,0 +1,74 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that a player's currentTime can be changed and that the AnimationsActor +// allows changing many players' currentTimes at once. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield testSetCurrentTime(walker, animations); + yield testSetCurrentTimes(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* testSetCurrentTime(walker, animations) { + info("Retrieve an animated node"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + + info("Retrieve the animation player for the node"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.setCurrentTime, "Player has the setCurrentTime method"); + + info("Check that the setCurrentTime method can be called"); + // Note that we don't check that it sets the animation to the right time here, + // this is too prone to intermittent failures, we'll do this later after + // pausing the animation. Here we merely test that the method doesn't fail. + yield player.setCurrentTime(player.initialState.currentTime + 1000); + + info("Pause the animation so we can really test if setCurrentTime works"); + yield player.pause(); + let pausedState = yield player.getCurrentState(); + + info("Set the current time to currentTime + 5s"); + yield player.setCurrentTime(pausedState.currentTime + 5000); + + let updatedState1 = yield player.getCurrentState(); + is(Math.round(updatedState1.currentTime - pausedState.currentTime), 5000, + "The currentTime was updated to +5s"); + + info("Set the current time to currentTime - 2s"); + yield player.setCurrentTime(updatedState1.currentTime - 2000); + let updatedState2 = yield player.getCurrentState(); + is(Math.round(updatedState2.currentTime - updatedState1.currentTime), -2000, + "The currentTime was updated to -2s"); +} + +function* testSetCurrentTimes(walker, animations) { + ok(animations.setCurrentTimes, "The AnimationsActor has the right method"); + + info("Retrieve multiple animated node and its animation players"); + + let nodeMulti = yield walker.querySelector(walker.rootNode, + ".multiple-animations"); + let players = (yield animations.getAnimationPlayersForNode(nodeMulti)); + + ok(players.length > 1, "Node has more than 1 animation player"); + + info("Try to set multiple current times at once"); + yield animations.setCurrentTimes(players, 500, true); + + info("Get the states of players and verify their correctness"); + for (let i = 0; i < players.length; i++) { + let state = yield players[i].getCurrentState(); + is(state.playState, "paused", `Player ${i + 1} is paused`); + is(state.currentTime, 500, `Player ${i + 1} has the right currentTime`); + } +} diff --git a/devtools/server/tests/browser/browser_animation_setPlaybackRate.js b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js new file mode 100644 index 000000000..b6d41b51e --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js @@ -0,0 +1,51 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that a player's playbackRate can be changed, and that multiple players +// can have their rates changed at the same time. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve an animated node"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + + info("Retrieve the animation player for the node"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.setPlaybackRate, "Player has the setPlaybackRate method"); + + info("Change the rate to 10"); + yield player.setPlaybackRate(10); + + info("Query the state again"); + let state = yield player.getCurrentState(); + is(state.playbackRate, 10, "The playbackRate was updated"); + + info("Change the rate back to 1"); + yield player.setPlaybackRate(1); + + info("Query the state again"); + state = yield player.getCurrentState(); + is(state.playbackRate, 1, "The playbackRate was changed back"); + + info("Retrieve several animation players and set their rates"); + node = yield walker.querySelector(walker.rootNode, "body"); + let players = yield animations.getAnimationPlayersForNode(node); + + info("Change all animations in to .5 rate"); + yield animations.setPlaybackRates(players, .5); + + info("Query their states and check they are correct"); + for (let player of players) { + let state = yield player.getCurrentState(); + is(state.playbackRate, .5, "The playbackRate was updated"); + } + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_simple.js b/devtools/server/tests/browser/browser_animation_simple.js new file mode 100644 index 000000000..52daf0084 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_simple.js @@ -0,0 +1,35 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Simple checks for the AnimationsActor + +add_task(function* () { + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8,test
"); + + ok(animations, "The AnimationsFront was created"); + ok(animations.getAnimationPlayersForNode, + "The getAnimationPlayersForNode method exists"); + ok(animations.toggleAll, "The toggleAll method exists"); + ok(animations.playAll, "The playAll method exists"); + ok(animations.pauseAll, "The pauseAll method exists"); + + let didThrow = false; + try { + yield animations.getAnimationPlayersForNode(null); + } catch (e) { + didThrow = true; + } + ok(didThrow, "An exception was thrown for a missing NodeActor"); + + let invalidNode = yield walker.querySelector(walker.rootNode, "title"); + let players = yield animations.getAnimationPlayersForNode(invalidNode); + ok(Array.isArray(players), "An array of players was returned"); + is(players.length, 0, "0 players have been returned for the invalid node"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_updatedState.js b/devtools/server/tests/browser/browser_animation_updatedState.js new file mode 100644 index 000000000..17d68e9e5 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_updatedState.js @@ -0,0 +1,55 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check the animation player's updated state + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playStateIsUpdatedDynamically(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playStateIsUpdatedDynamically(walker, animations) { + info("Getting the test node (which runs a very long animation)"); + // The animation lasts for 100s, to avoid intermittents. + let node = yield walker.querySelector(walker.rootNode, ".long-animation"); + + info("Getting the animation player front for this node"); + let [player] = yield animations.getAnimationPlayersForNode(node); + yield player.ready(); + + let state = yield player.getCurrentState(); + is(state.playState, "running", + "The playState is running while the animation is running"); + + info("Change the animation's currentTime to be near the end and wait for " + + "it to finish"); + let onFinished = waitForAnimationPlayState(player, "finished"); + // Set the currentTime to 98s, knowing that the animation lasts for 100s. + yield player.setCurrentTime(98 * 1000); + state = yield onFinished; + is(state.playState, "finished", + "The animation has ended and the state has been updated"); + ok(state.currentTime > player.initialState.currentTime, + "The currentTime has been updated"); +} + +function* waitForAnimationPlayState(player, playState) { + let state = {}; + while (state.playState !== playState) { + state = yield player.getCurrentState(); + yield wait(500); + } + return state; +} + +function wait(ms) { + return new Promise(r => setTimeout(r, ms)); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_01.js b/devtools/server/tests/browser/browser_canvasframe_helper_01.js new file mode 100644 index 000000000..7fd943197 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_01.js @@ -0,0 +1,90 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Simple CanvasFrameAnonymousContentHelper tests. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + child.textContent = "test element"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + ok(helper.content instanceof AnonymousContent, + "The helper owns the AnonymousContent object"); + ok(helper.getTextContentForElement, + "The helper has the getTextContentForElement method"); + ok(helper.setTextContentForElement, + "The helper has the setTextContentForElement method"); + ok(helper.setAttributeForElement, + "The helper has the setAttributeForElement method"); + ok(helper.getAttributeForElement, + "The helper has the getAttributeForElement method"); + ok(helper.removeAttributeForElement, + "The helper has the removeAttributeForElement method"); + ok(helper.addEventListenerForElement, + "The helper has the addEventListenerForElement method"); + ok(helper.removeEventListenerForElement, + "The helper has the removeEventListenerForElement method"); + ok(helper.getElement, + "The helper has the getElement method"); + ok(helper.scaleRootElement, + "The helper has the scaleRootElement method"); + + is(helper.getTextContentForElement("child-element"), "test element", + "The text content was retrieve correctly"); + is(helper.getAttributeForElement("child-element", "id"), "child-element", + "The ID attribute was retrieve correctly"); + is(helper.getAttributeForElement("child-element", "class"), "child-element", + "The class attribute was retrieve correctly"); + + let el = helper.getElement("child-element"); + ok(el, "The DOMNode-like element was created"); + + is(el.getTextContent(), "test element", + "The text content was retrieve correctly"); + is(el.getAttribute("id"), "child-element", + "The ID attribute was retrieve correctly"); + is(el.getAttribute("class"), "child-element", + "The class attribute was retrieve correctly"); + + info("Destroying the helper"); + helper.destroy(); + env.destroy(); + + ok(!helper.getTextContentForElement("child-element"), + "No text content was retrieved after the helper was destroyed"); + ok(!helper.getAttributeForElement("child-element", "id"), + "No ID attribute was retrieved after the helper was destroyed"); + ok(!helper.getAttributeForElement("child-element", "class"), + "No class attribute was retrieved after the helper was destroyed"); + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_02.js b/devtools/server/tests/browser/browser_canvasframe_helper_02.js new file mode 100644 index 000000000..90400c50a --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_02.js @@ -0,0 +1,48 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the CanvasFrameAnonymousContentHelper does not insert content in +// XUL windows. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +add_task(function* () { + let browser = yield addTab("about:preferences"); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + child.textContent = "test element"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + ok(!helper.content, "The AnonymousContent was not inserted in the window"); + ok(!helper.getTextContentForElement("child-element"), + "No text content is returned"); + + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_03.js b/devtools/server/tests/browser/browser_canvasframe_helper_03.js new file mode 100644 index 000000000..85e27c7de --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_03.js @@ -0,0 +1,102 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the CanvasFrameAnonymousContentHelper event handling mechanism. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + let el = helper.getElement("child-element"); + + info("Adding an event listener on the inserted element"); + let mouseDownHandled = 0; + function onMouseDown(e, id) { + is(id, "child-element", "The mousedown event was triggered on the element"); + ok(!e.originalTarget, "The originalTarget property isn't available"); + mouseDownHandled++; + } + el.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the inserted element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, "The mousedown event was handled once on the element"); + + info("Synthesizing an event somewhere else"); + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(400, 400, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, "The mousedown event was not handled on the element"); + + info("Removing the event listener"); + el.removeEventListener("mousedown", onMouseDown); + + info("Synthesizing another event after the listener has been removed"); + // Using a document event listener to know when the event has been synthesized. + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, + "The mousedown event hasn't been handled after the listener was removed"); + + info("Adding again the event listener"); + el.addEventListener("mousedown", onMouseDown); + + info("Destroying the helper"); + env.destroy(); + helper.destroy(); + + info("Synthesizing another event after the helper has been destroyed"); + // Using a document event listener to know when the event has been synthesized. + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, + "The mousedown event hasn't been handled after the helper was destroyed"); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_04.js b/devtools/server/tests/browser/browser_canvasframe_helper_04.js new file mode 100644 index 000000000..d038f84a0 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_04.js @@ -0,0 +1,98 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the CanvasFrameAnonymousContentHelper re-inserts the content when the +// page reloads. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); +const events = require("sdk/event/core"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL_1 = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 1"; +const TEST_URL_2 = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 2"; + +add_task(function* () { + let browser = yield addTab(TEST_URL_2); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + child.textContent = "test content"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + info("Get an element from the helper"); + let el = helper.getElement("child-element"); + + info("Try to access the element"); + is(el.getAttribute("class"), "child-element", + "The attribute is correct before navigation"); + is(el.getTextContent(), "test content", + "The text content is correct before navigation"); + + info("Add an event listener on the element"); + let mouseDownHandled = 0; + function onMouseDown(e, id) { + is(id, "child-element", "The mousedown event was triggered on the element"); + mouseDownHandled++; + } + el.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + is(mouseDownHandled, 1, "The mousedown event was handled once before navigation"); + + info("Navigating to a new page"); + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + content.location = TEST_URL_2; + yield loaded; + doc = gBrowser.selectedBrowser.contentWindow.document; + + info("Try to access the element again"); + is(el.getAttribute("class"), "child-element", + "The attribute is correct after navigation"); + is(el.getTextContent(), "test content", + "The text content is correct after navigation"); + + info("Synthesizing an event on the element again"); + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + is(mouseDownHandled, 1, "The mousedown event was not handled after navigation"); + + info("Destroying the helper"); + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_05.js b/devtools/server/tests/browser/browser_canvasframe_helper_05.js new file mode 100644 index 000000000..94fb66914 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_05.js @@ -0,0 +1,112 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test some edge cases of the CanvasFrameAnonymousContentHelper event handling +// mechanism. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + + let parent = doc.createElement("div"); + parent.style = "pointer-events:auto;width:300px;height:300px;background:yellow;"; + parent.id = "parent-element"; + root.appendChild(parent); + + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + parent.appendChild(child); + + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + info("Getting the parent and child elements"); + let parentEl = helper.getElement("parent-element"); + let childEl = helper.getElement("child-element"); + + info("Adding an event listener on both elements"); + let mouseDownHandled = []; + function onMouseDown(e, id) { + mouseDownHandled.push(id); + } + parentEl.addEventListener("mousedown", onMouseDown); + childEl.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the child element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 2, "The mousedown event was handled twice"); + is(mouseDownHandled[0], "child-element", + "The mousedown event was handled on the child element"); + is(mouseDownHandled[1], "parent-element", + "The mousedown event was handled on the parent element"); + + info("Synthesizing an event on the parent, outside of the child element"); + mouseDownHandled = []; + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(250, 250, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled only once"); + is(mouseDownHandled[0], "parent-element", + "The mousedown event was handled on the parent element"); + + info("Removing the event listener"); + parentEl.removeEventListener("mousedown", onMouseDown); + childEl.removeEventListener("mousedown", onMouseDown); + + info("Adding an event listener on the parent element only"); + mouseDownHandled = []; + parentEl.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the child element"); + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled once"); + is(mouseDownHandled[0], "parent-element", + "The mousedown event did bubble to the parent element"); + + info("Removing the parent listener"); + parentEl.removeEventListener("mousedown", onMouseDown); + + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_06.js b/devtools/server/tests/browser/browser_canvasframe_helper_06.js new file mode 100644 index 000000000..2b137fe26 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_06.js @@ -0,0 +1,100 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test support for event propagation stop in the +// CanvasFrameAnonymousContentHelper event handling mechanism. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + + let parent = doc.createElement("div"); + parent.style = "pointer-events:auto;width:300px;height:300px;background:yellow;"; + parent.id = "parent-element"; + root.appendChild(parent); + + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + parent.appendChild(child); + + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + info("Getting the parent and child elements"); + let parentEl = helper.getElement("parent-element"); + let childEl = helper.getElement("child-element"); + + info("Adding an event listener on both elements"); + let mouseDownHandled = []; + + function onParentMouseDown(e, id) { + mouseDownHandled.push(id); + } + parentEl.addEventListener("mousedown", onParentMouseDown); + + function onChildMouseDown(e, id) { + mouseDownHandled.push(id); + e.stopPropagation(); + } + childEl.addEventListener("mousedown", onChildMouseDown); + + info("Synthesizing an event on the child element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled only once"); + is(mouseDownHandled[0], "child-element", + "The mousedown event was handled on the child element"); + + info("Synthesizing an event on the parent, outside of the child element"); + mouseDownHandled = []; + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(250, 250, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled only once"); + is(mouseDownHandled[0], "parent-element", + "The mousedown event was handled on the parent element"); + + info("Removing the event listener"); + parentEl.removeEventListener("mousedown", onParentMouseDown); + childEl.removeEventListener("mousedown", onChildMouseDown); + + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_directorscript_actors.js b/devtools/server/tests/browser/browser_directorscript_actors.js new file mode 100644 index 000000000..bdfc8f8f1 --- /dev/null +++ b/devtools/server/tests/browser/browser_directorscript_actors.js @@ -0,0 +1,159 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {DirectorManagerFront} = require("devtools/shared/fronts/director-manager"); +const {DirectorRegistry} = require("devtools/server/actors/director-registry"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "director-script-target.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + DirectorRegistry.clear(); + let directorManager = DirectorManagerFront(client, form); + + yield testDirectorScriptAttachEventAttributes(directorManager); + yield testDirectorScriptMessagePort(directorManager); + yield testDirectorScriptWindowEval(directorManager); + yield testDirectorScriptUnloadOnDetach(directorManager); + + yield client.close(); + gBrowser.removeCurrentTab(); + DirectorRegistry.clear(); +}); + +function* testDirectorScriptAttachEventAttributes(directorManager) { + let attachEvent = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_attachEventAttributes", + scriptCode: "(" + (function () { + exports.attach = function () {}; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + let { directorScriptId, url } = attachEvent; + + is(directorScriptId, "testDirectorScript_attachEventAttributes", + "attach event should contains directorScriptId"); + is(url, MAIN_DOMAIN + "director-script-target.html"); +} + +function* testDirectorScriptMessagePort(directorManager) { + let { port } = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_MessagePort", + scriptCode: "(" + (function () { + exports.attach = function ({port}) { + port.onmessage = function (evt) { + // echo messages + evt.source.postMessage(evt.data); + }; + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + ok(port && port.postMessage, "testDirector_MessagePort port received"); + + // exchange messages over the MessagePort + let waitForMessagePortEvent = once(port, "message"); + // needs to explicit start the port + port.start(); + + var msg = { k1: "v1", k2: [1, 2, 3] }; + port.postMessage(msg); + + var reply = yield waitForMessagePortEvent; + + is(JSON.stringify(reply.data), JSON.stringify(msg), "echo reply received on the MessagePortClient"); +} + +function* testDirectorScriptWindowEval(directorManager) { + let { port } = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_WindowEval", + scriptCode: "(" + (function () { + exports.attach = function ({window, port}) { + var onpageloaded = function () { + var globalVarValue = window.eval("globalAccessibleVar;"); + port.postMessage(globalVarValue); + }; + + if (window.document && window.document.readyState === "complete") { + onpageloaded(); + } else { + window.addEventListener("load", onpageloaded, false); + } + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + ok(port, "testDirectorScript_WindowEval port received"); + + // exchange messages over the MessagePort + let waitForMessagePortEvent = once(port, "message"); + // needs to explicit start the port + port.start(); + + var portEvent = yield waitForMessagePortEvent; + + ok(portEvent.data !== "unsecure-eval", "window.eval should be wrapped and safe"); + is(portEvent.data, "global-value", "globalAccessibleVar should be accessible through window.eval"); +} + +function* testDirectorScriptUnloadOnDetach(directorManager) { + let { port } = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_unloadOnDetach", + scriptCode: "(" + (function () { + exports.attach = function ({port, onUnload}) { + onUnload(function () { + port.postMessage("ONUNLOAD"); + }); + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + ok(port, "testDirectorScript_unloadOnDetach port received"); + port.start(); + + let waitForDetach = once(directorManager, "director-script-detach"); + let waitForMessage = once(port, "message"); + + directorManager.disableByScriptIds(["testDirectorScript_unloadOnDetach"], {reload: false}); + + let { directorScriptId } = yield waitForDetach; + is(directorScriptId, "testDirectorScript_unloadOnDetach", + "detach event should contains directorScriptId"); + + let portEvent = yield waitForMessage; + is(portEvent.data, "ONUNLOAD", "director-script's exports.onUnload called on detach"); +} + +function* installAndEnableDirectorScript(directorManager, directorScriptDef) { + let { scriptId } = directorScriptDef; + + DirectorRegistry.install(scriptId, directorScriptDef); + + let waitForAttach = once(directorManager, "director-script-attach"); + let waitForError = once(directorManager, "director-script-error"); + + directorManager.enableByScriptIds([scriptId], {reload: false}); + + let attachOrErrorEvent = yield Promise.race([waitForAttach, waitForError]); + + return attachOrErrorEvent; +} diff --git a/devtools/server/tests/browser/browser_directorscript_actors_error_events.js b/devtools/server/tests/browser/browser_directorscript_actors_error_events.js new file mode 100644 index 000000000..0afe16388 --- /dev/null +++ b/devtools/server/tests/browser/browser_directorscript_actors_error_events.js @@ -0,0 +1,132 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {DirectorManagerFront} = require("devtools/shared/fronts/director-manager"); +const {DirectorRegistry} = require("devtools/server/actors/director-registry"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "director-script-target.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + DirectorRegistry.clear(); + let directorManager = DirectorManagerFront(client, form); + + yield testErrorOnRequire(directorManager); + yield testErrorOnEvaluate(directorManager); + yield testErrorOnAttach(directorManager); + yield testErrorOnDetach(directorManager); + + yield client.close(); + gBrowser.removeCurrentTab(); + DirectorRegistry.clear(); +}); + +function* testErrorOnRequire(directorManager) { + // director script require method should raise a "not implemented" exception + let errorOnRequire = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnRequire", + scriptCode: "(" + (function () { + // this director script should generate an error event + // because require raise a "not implemented" exception + require("fake_module"); + }).toString() + ")();", + scriptOptions: {} + }); + + assertIsDirectorScriptError(errorOnRequire); + + let { message } = errorOnRequire; + is(message, "Error: NOT IMPLEMENTED", "error.message contains the expected error message"); +} + +function* testErrorOnEvaluate(directorManager) { + // director scripts should send an error events if the director script raise an exception on + // evaluation + let errorOnEvaluate = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnEvaluate", + scriptCode: "(" + (function () { + // this will raise an exception evaluating + // the director script + raise.an_error.during.content_script.load(); + }).toString() + ")();", + scriptOptions: {} + }); + assertIsDirectorScriptError(errorOnEvaluate); +} + +function* testErrorOnAttach(directorManager) { + // director scripts should send an error events if the director script raise an exception on + // evaluation + let errorOnAttach = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnAttach", + scriptCode: "(" + (function () { + // this will raise an exception on evaluating + // the director script + module.exports = function () { + raise.an_error.during.content_script.load(); + }; + }).toString() + ")();", + scriptOptions: {} + }); + assertIsDirectorScriptError(errorOnAttach); +} + +function* testErrorOnDetach(directorManager) { + // director scripts should send an error events if the director script raise an exception on + // evaluation + let attach = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnDetach", + scriptCode: "(" + (function () { + module.exports = function ({onUnload}) { + // this will raise an exception on unload the director script + onUnload(function () { + raise_an_error_onunload(); + }); + }; + }).toString() + ")();", + scriptOptions: {} + }); + + let waitForDetach = once(directorManager, "director-script-detach"); + let waitForError = once(directorManager, "director-script-error"); + + directorManager.disableByScriptIds(["testDirectorScript_errorOnDetach"], {reload: false}); + + let detach = yield waitForDetach; + let error = yield waitForError; + ok(detach, "testDirectorScript_errorOnDetach detach event received"); + ok(error, "testDirectorScript_errorOnDetach detach error received"); + assertIsDirectorScriptError(error); +} + +function* installAndEnableDirectorScript(directorManager, directorScriptDef) { + let { scriptId } = directorScriptDef; + + DirectorRegistry.install(scriptId, directorScriptDef); + + let waitForAttach = once(directorManager, "director-script-attach"); + let waitForError = once(directorManager, "director-script-error"); + + directorManager.enableByScriptIds([scriptId], {reload: false}); + + let attachOrErrorEvent = yield Promise.race([waitForAttach, waitForError]); + + return attachOrErrorEvent; +} + +function assertIsDirectorScriptError(error) { + ok(!!error.message, "errors should contain a message"); + ok(!!error.stack, "errors should contain a stack trace"); + ok(!!error.fileName, "errors should contain a fileName"); + ok(typeof error.columnNumber == "number", "errors should contain a columnNumber"); + ok(typeof error.lineNumber == "number", "errors should contain a lineNumber"); + + ok(!!error.directorScriptId, "errors should contain a directorScriptId"); +} diff --git a/devtools/server/tests/browser/browser_directorscript_actors_exports.js b/devtools/server/tests/browser/browser_directorscript_actors_exports.js new file mode 100644 index 000000000..f9ef56f51 --- /dev/null +++ b/devtools/server/tests/browser/browser_directorscript_actors_exports.js @@ -0,0 +1,87 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {DirectorManagerFront} = require("devtools/shared/fronts/director-manager"); +const {DirectorRegistry} = require("devtools/server/actors/director-registry"); + +DirectorRegistry.clear(); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "director-script-target.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + DirectorRegistry.clear(); + let directorManager = DirectorManagerFront(client, form); + + // director scripts attach method defaults to module.exports + let attachModuleExports = yield testDirectorScriptExports(directorManager, { + scriptId: "testDirectorScript_moduleExports", + scriptCode: "(" + (function () { + module.exports = function () {}; + }).toString() + ")();", + scriptOptions: {} + }); + ok(attachModuleExports.port, "testDirectorScript_moduleExports attach event received"); + + // director scripts attach method can be configured using the attachMethod scriptOptions + let attachExportsAttach = yield testDirectorScriptExports(directorManager, { + scriptId: "testDirectorScript_exportsAttach", + scriptCode: "(" + (function () { + exports.attach = function () {}; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + ok(attachExportsAttach.port, "testDirectorScript_exportsAttach attach event received"); + + // director scripts without an attach method generates an error event + let errorUndefinedAttachMethod = yield testDirectorScriptExports(directorManager, { + scriptId: "testDirectorScript_undefinedAttachMethod", + scriptCode: "(" + (function () { + // this director script should raise an error + // because it doesn't export any attach method + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + let { message } = errorUndefinedAttachMethod; + ok(!!message, "testDirectorScript_undefinedAttachMethod error event received"); + assertIsDirectorScriptError(errorUndefinedAttachMethod); + + yield client.close(); + gBrowser.removeCurrentTab(); + DirectorRegistry.clear(); +}); + +function assertIsDirectorScriptError(error) { + ok(!!error.message, "errors should contain a message"); + ok(!!error.stack, "errors should contain a stack trace"); + ok(!!error.fileName, "errors should contain a fileName"); + ok(typeof error.columnNumber == "number", "errors should contain a columnNumber"); + ok(typeof error.lineNumber == "number", "errors should contain a lineNumber"); + + ok(!!error.directorScriptId, "errors should contain a directorScriptId"); +} + +function* testDirectorScriptExports(directorManager, directorScriptDef) { + let { scriptId } = directorScriptDef; + + DirectorRegistry.install(scriptId, directorScriptDef); + + let waitForAttach = once(directorManager, "director-script-attach"); + let waitForError = once(directorManager, "director-script-error"); + directorManager.enableByScriptIds([scriptId], {reload: false}); + + let attachOrErrorEvent = yield Promise.race([waitForAttach, waitForError]); + + return attachOrErrorEvent; +} diff --git a/devtools/server/tests/browser/browser_markers-cycle-collection.js b/devtools/server/tests/browser/browser_markers-cycle-collection.js new file mode 100644 index 000000000..dc33375f2 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-cycle-collection.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "nsCycleCollector::Collect" and + * "nsCycleCollector::ForgetSkippable" markers when we force cycle collection. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + // This test runs very slowly on linux32 debug EC2 instances. + requestLongerTimeout(2); + + let browser = yield addTab(MAIN_DOMAIN + "doc_force_cc.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, ["nsCycleCollector::Collect", "nsCycleCollector::ForgetSkippable"]); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === "nsCycleCollector::Collect"), "got some nsCycleCollector::Collect markers"); + ok(markers.some(m => m.name === "nsCycleCollector::ForgetSkippable"), "got some nsCycleCollector::Collect markers"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-01.js b/devtools/server/tests/browser/browser_markers-docloading-01.js new file mode 100644 index 000000000..3c82d56c4 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-01.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withMarkers: true }); + + front.once("doc-loading", e => { + ok(false, "Should not be emitting doc-loading events."); + }); + + executeSoon(() => doc.location.reload()); + + yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers"); + yield front.stop(rec); + + ok(true, "Found the required marker names."); + + // Wait some more time to make sure the 'doc-loading' events never get fired. + yield DevToolsUtils.waitForTime(1000); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-02.js b/devtools/server/tests/browser/browser_markers-docloading-02.js new file mode 100644 index 000000000..0142ea0cd --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-02.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withMarkers: true, withDocLoadingEvents: true }); + + yield new Promise(resolve => { + front.once("doc-loading", resolve); + doc.location.reload(); + }); + + ok(true, "At least one doc-loading event got fired."); + + yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers"); + yield front.stop(rec); + + ok(true, "Found the required marker names."); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-03.js b/devtools/server/tests/browser/browser_markers-docloading-03.js new file mode 100644 index 000000000..1960da4da --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-03.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withDocLoadingEvents: true }); + + waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers").then(e => { + ok(false, "Should not be emitting doc-loading markers."); + }); + + yield new Promise(resolve => { + front.once("doc-loading", resolve); + doc.location.reload(); + }); + + ok(true, "At least one doc-loading event got fired."); + + yield front.stop(rec); + + // Wait some more time to make sure the 'doc-loading' markers never get fired. + yield DevToolsUtils.waitForTime(1000); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-gc.js b/devtools/server/tests/browser/browser_markers-gc.js new file mode 100644 index 000000000..559b19161 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-gc.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "GarbageCollection" markers. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const MARKER_NAME = "GarbageCollection"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_force_gc.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, MARKER_NAME); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`); + ok(markers.every(({causeName}) => typeof causeName === "string"), + "All markers have a causeName."); + ok(markers.every(({cycle}) => typeof cycle === "number"), + "All markers have a `cycle` ID."); + + markers = rec.getMarkers(); + + // Bug 1197646 + let ordered = true; + markers.reduce((previousStart, current, i) => { + if (i === 0) { + return current.start; + } + if (current.start < previousStart) { + ok(false, `markers must be in order. ${current.name} marker has later start time (${current.start}) thanprevious: ${previousStart}`); + ordered = false; + } + return current.start; + }); + + is(ordered, true, "All GC and non-GC markers are in order by start time."); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-minor-gc.js b/devtools/server/tests/browser/browser_markers-minor-gc.js new file mode 100644 index 000000000..332764348 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-minor-gc.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "MinorGC" markers when we continue to steadily allocate + * objects. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + // This test runs very slowly on linux32 debug EC2 instances. + requestLongerTimeout(2); + + let doc = yield addTab(MAIN_DOMAIN + "doc_allocations.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, ["MinorGC"]); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === "MinorGC" && m.causeName), + "got some MinorGC markers"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-parse-html.js b/devtools/server/tests/browser/browser_markers-parse-html.js new file mode 100644 index 000000000..bd4f479c0 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-parse-html.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "Parse HTML" markers. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const MARKER_NAME = "Parse HTML"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, MARKER_NAME); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-styles.js b/devtools/server/tests/browser/browser_markers-styles.js new file mode 100644 index 000000000..a3dffe8b5 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-styles.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "Styles" markers with correct meta. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const MARKER_NAME = "Styles"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, MARKER_NAME, function (markers) { + return markers.some(({restyleHint}) => restyleHint != void 0); + }); + + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`); + ok(markers.some(({restyleHint}) => restyleHint != void 0), + "Some markers have a restyleHint."); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-timestamp.js b/devtools/server/tests/browser/browser_markers-timestamp.js new file mode 100644 index 000000000..428499502 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-timestamp.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get a "TimeStamp" marker. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmConsoleMethod, pmmLoadFrameScripts, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); +const MARKER_NAME = "TimeStamp"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + pmmLoadFrameScripts(gBrowser); + pmmConsoleMethod("timeStamp"); + pmmConsoleMethod("timeStamp", "myLabel"); + + let markers = yield waitForMarkerType(front, MARKER_NAME, markers => markers.length >= 2); + + yield front.stopRecording(rec); + + ok(markers.every(({stack}) => typeof stack === "number"), "All markers have stack references."); + ok(markers.every(({name}) => name === "TimeStamp"), "All markers found are TimeStamp markers"); + ok(markers.length === 2, "found 2 TimeStamp markers"); + ok(markers.every(({start, end}) => typeof start === "number" && start === end), + "All markers have equal start and end times"); + is(markers[0].causeName, void 0, "Unlabeled timestamps have an empty causeName"); + is(markers[1].causeName, "myLabel", "Labeled timestamps have correct causeName"); + + pmmClearFrameScripts(); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_navigateEvents.js b/devtools/server/tests/browser/browser_navigateEvents.js new file mode 100644 index 000000000..f8652f197 --- /dev/null +++ b/devtools/server/tests/browser/browser_navigateEvents.js @@ -0,0 +1,160 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL1 = MAIN_DOMAIN + "navigate-first.html"; +const URL2 = MAIN_DOMAIN + "navigate-second.html"; + +var events = require("sdk/event/core"); +var client; + +SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]}); + +// State machine to check events order +var i = 0; +function assertEvent(event, data) { + let x = 0; + switch (i++) { + case x++: + is(event, "request", "Get first page load"); + is(data, URL1); + break; + case x++: + is(event, "load-new-document", "Ask to load the second page"); + break; + case x++: + is(event, "unload-dialog", "We get the dialog on first page unload"); + break; + case x++: + is(event, "will-navigate", "The very first event is will-navigate on server side"); + is(data.newURI, URL2, "newURI property is correct"); + break; + case x++: + is(event, "request", "RDP is async with messageManager, the request happens after will-navigate"); + is(data, URL2); + break; + case x++: + is(event, "tabNavigated", "After the request, the client receive tabNavigated"); + is(data.state, "start", "state is start"); + is(data.url, URL2, "url property is correct"); + is(data.nativeConsoleAPI, true, "nativeConsoleAPI is correct"); + break; + case x++: + is(event, "DOMContentLoaded"); + is(content.document.readyState, "interactive"); + break; + case x++: + is(event, "load"); + is(content.document.readyState, "complete"); + break; + case x++: + is(event, "navigate", "Then once the second doc is loaded, we get the navigate event"); + is(content.document.readyState, "complete", "navigate is emitted only once the document is fully loaded"); + break; + case x++: + is(event, "tabNavigated", "Finally, the receive the client event"); + is(data.state, "stop", "state is stop"); + is(data.url, URL2, "url property is correct"); + is(data.nativeConsoleAPI, true, "nativeConsoleAPI is correct"); + + // End of test! + cleanup(); + break; + } +} + +function waitForOnBeforeUnloadDialog(browser, callback) { + browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog() { + browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true); + + executeSoon(() => { + let stack = browser.parentNode; + let dialogs = stack.getElementsByTagName("tabmodalprompt"); + let {button0, button1} = dialogs[0].ui; + callback(button0, button1); + }); + }, true); +} + +var httpObserver = function (subject, topic, state) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let url = channel.URI.spec; + // Only listen for our document request, as many other requests can happen + if (url == URL1 || url == URL2) { + assertEvent("request", url); + } +}; +Services.obs.addObserver(httpObserver, "http-on-modify-request", false); + +function onDOMContentLoaded() { + assertEvent("DOMContentLoaded"); +} +function onLoad() { + assertEvent("load"); +} + +function getServerTabActor(callback) { + // Ensure having a minimal server + initDebuggerServer(); + + // Connect to this tab + let transport = DebuggerServer.connectPipe(); + client = new DebuggerClient(transport); + connectDebuggerClient(client).then(form => { + let actorID = form.actor; + client.attachTab(actorID, function (aResponse, aTabClient) { + // !Hack! Retrieve a server side object, the BrowserTabActor instance + let tabActor = DebuggerServer._searchAllConnectionsForActor(actorID); + callback(tabActor); + }); + }); + + client.addListener("tabNavigated", function (aEvent, aPacket) { + assertEvent("tabNavigated", aPacket); + }); +} + +function test() { + // Open a test tab + addTab(URL1).then(function (browser) { + let doc = browser.contentDocument; + getServerTabActor(function (tabActor) { + // In order to listen to internal will-navigate/navigate events + events.on(tabActor, "will-navigate", function (data) { + assertEvent("will-navigate", data); + }); + events.on(tabActor, "navigate", function (data) { + assertEvent("navigate", data); + }); + + // Start listening for page load events + browser.addEventListener("DOMContentLoaded", onDOMContentLoaded, true); + browser.addEventListener("load", onLoad, true); + + // Listen for alert() call being made in navigate-first during unload + waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) { + assertEvent("unload-dialog"); + // accept to quit this page to another + btnLeave.click(); + }); + + // Load another document in this doc to dispatch these events + assertEvent("load-new-document"); + content.location = URL2; + }); + + }); +} + +function cleanup() { + let browser = gBrowser.selectedBrowser; + browser.removeEventListener("DOMContentLoaded", onDOMContentLoaded); + browser.removeEventListener("load", onLoad); + client.close().then(function () { + Services.obs.addObserver(httpObserver, "http-on-modify-request", false); + DebuggerServer.destroy(); + finish(); + }); +} diff --git a/devtools/server/tests/browser/browser_perf-allocation-data.js b/devtools/server/tests/browser/browser_perf-allocation-data.js new file mode 100644 index 000000000..3d4a94dee --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-allocation-data.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we have allocation data coming from the front. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_allocations.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let rec = yield front.startRecording({ withMarkers: true, withAllocations: true, withTicks: true }); + + yield waitUntil(() => rec.getAllocations().frames.length); + yield waitUntil(() => rec.getAllocations().timestamps.length); + yield waitUntil(() => rec.getAllocations().sizes.length); + yield waitUntil(() => rec.getAllocations().sites.length); + + yield front.stopRecording(rec); + + let { frames, timestamps, sizes, sites } = rec.getAllocations(); + + is(timestamps.length, sizes.length, "we have the same amount of timestamps and sizes"); + ok(timestamps.every(time => time > 0 && typeof time === "number"), "all timestamps have numeric values"); + ok(sizes.every(n => n > 0 && typeof n === "number"), "all sizes are positive numbers"); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-profiler-01.js b/devtools/server/tests/browser/browser_perf-profiler-01.js new file mode 100644 index 000000000..36d200f01 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-profiler-01.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the profiler connection front does not activate the built-in + * profiler module if not necessary, and doesn't deactivate it when + * a recording is stopped. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmIsProfilerActive, pmmStopProfiler, pmmLoadFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + pmmLoadFrameScripts(gBrowser); + + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should not have been automatically started."); + + let rec = yield front.startRecording(); + yield front.stopRecording(rec); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active (1)."); + + rec = yield front.startRecording(); + yield front.stopRecording(rec); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active (2)."); + + yield front.destroy(); + yield client.close(); + + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should no longer be active."); + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-profiler-02.js b/devtools/server/tests/browser/browser_perf-profiler-02.js new file mode 100644 index 000000000..29842cef7 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-profiler-02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the built-in profiler module doesn't deactivate when the toolbox + * is destroyed if there are other consumers using it. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmIsProfilerActive, pmmStopProfiler, pmmLoadFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); + +add_task(function* () { + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let firstFront = PerformanceFront(client, form); + yield firstFront.connect(); + + pmmLoadFrameScripts(gBrowser); + + yield firstFront.startRecording(); + + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let client2 = new DebuggerClient(DebuggerServer.connectPipe()); + let form2 = yield connectDebuggerClient(client2); + let secondFront = PerformanceFront(client2, form2); + yield secondFront.connect(); + pmmLoadFrameScripts(gBrowser); + + yield secondFront.startRecording(); + + // Manually teardown the tabs so we can check profiler status + yield secondFront.destroy(); + yield client2.close(); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active."); + + yield firstFront.destroy(); + yield client.close(); + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should no longer be active."); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-profiler-03.js b/devtools/server/tests/browser/browser_perf-profiler-03.js new file mode 100644 index 000000000..28d87fe45 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-profiler-03.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the built-in profiler module is not reactivated if no other + * consumer was using it over the remote debugger protocol, and ensures + * that the actor will work properly even in such cases (e.g. the Gecko Profiler + * addon was installed and automatically activated the profiler module). + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmIsProfilerActive, pmmStartProfiler, pmmStopProfiler, pmmLoadFrameScripts, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); + +add_task(function* () { + // Ensure the profiler is already running when the test starts. + pmmLoadFrameScripts(gBrowser); + let entries = 1000000; + let interval = 1; + let features = ["js"]; + yield pmmStartProfiler({ entries, interval, features }); + + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active."); + + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let firstFront = PerformanceFront(client, form); + yield firstFront.connect(); + + let recording = yield firstFront.startRecording(); + + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let client2 = new DebuggerClient(DebuggerServer.connectPipe()); + let form2 = yield connectDebuggerClient(client2); + let secondFront = PerformanceFront(client2, form2); + yield secondFront.connect(); + + yield secondFront.destroy(); + yield client2.close(); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active."); + + yield firstFront.destroy(); + yield client.close(); + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should have been automatically stopped."); + + pmmClearFrameScripts(); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-realtime-markers.js b/devtools/server/tests/browser/browser_perf-realtime-markers.js new file mode 100644 index 000000000..b9eab211c --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-realtime-markers.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test functionality of real time markers. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let lastMemoryDelta = 0; + let lastTickDelta = 0; + + let counters = { + markers: [], + memory: [], + ticks: [] + }; + + let deferreds = { + markers: defer(), + memory: defer(), + ticks: defer() + }; + + front.on("timeline-data", handler); + + let rec = yield front.startRecording({ withMarkers: true, withMemory: true, withTicks: true }); + yield Promise.all(Object.keys(deferreds).map(type => deferreds[type].promise)); + yield front.stopRecording(rec); + front.off("timeline-data", handler); + + is(counters.markers.length, 1, "one marker event fired."); + is(counters.memory.length, 3, "three memory events fired."); + is(counters.ticks.length, 3, "three ticks events fired."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); + + function handler(name, data) { + if (name === "markers") { + if (counters.markers.length >= 1) { return; } + ok(data.markers[0].start, "received atleast one marker with `start`"); + ok(data.markers[0].end, "received atleast one marker with `end`"); + ok(data.markers[0].name, "received atleast one marker with `name`"); + + counters.markers.push(data.markers); + } + else if (name === "memory") { + if (counters.memory.length >= 3) { return; } + let { delta, measurement } = data; + is(typeof delta, "number", "received `delta` in memory event"); + ok(delta > lastMemoryDelta, "received `delta` in memory event"); + ok(measurement.total, "received `total` in memory event"); + + counters.memory.push({ delta, measurement }); + lastMemoryDelta = delta; + } + else if (name === "ticks") { + if (counters.ticks.length >= 3) { return; } + let { delta, timestamps } = data; + ok(delta > lastTickDelta, "received `delta` in ticks event"); + + // Timestamps aren't guaranteed to always contain tick events, since + // they're dependent on the refresh driver, which may be blocked. + + counters.ticks.push({ delta, timestamps }); + lastTickDelta = delta; + } + else if (name === "frames") { + // Nothing to do here. + } + else { + ok(false, `Received unknown event: ${name}`); + } + + if (name === "markers" && counters[name].length === 1 || + name === "memory" && counters[name].length === 3 || + name === "ticks" && counters[name].length === 3) { + deferreds[name].resolve(); + } + } +}); diff --git a/devtools/server/tests/browser/browser_perf-recording-actor-01.js b/devtools/server/tests/browser/browser_perf-recording-actor-01.js new file mode 100644 index 000000000..683493121 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-recording-actor-01.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the state of a recording rec from start to finish for recording, + * completed, and rec data. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let rec = yield front.startRecording({ withMarkers: true, withTicks: true, withMemory: true }); + ok(rec.isRecording(), "RecordingModel is recording when created"); + yield busyWait(100); + yield waitUntil(() => rec.getMemory().length); + ok(true, "RecordingModel populates memory while recording"); + yield waitUntil(() => rec.getTicks().length); + ok(true, "RecordingModel populates ticks while recording"); + yield waitUntil(() => rec.getMarkers().length); + ok(true, "RecordingModel populates markers while recording"); + + ok(!rec.isCompleted(), "RecordingModel is not completed when still recording"); + + let stopping = once(front, "recording-stopping"); + let stopped = once(front, "recording-stopped"); + front.stopRecording(rec); + + yield stopping; + ok(!rec.isRecording(), "on 'recording-stopping', model is no longer recording"); + // This handler should be called BEFORE "recording-stopped" is called, as + // there is some delay, but in the event where "recording-stopped" finishes + // before we check here, ensure that we're atleast in the right state + if (rec.getProfile()) { + ok(rec.isCompleted(), "recording is completed once it has profile data"); + } else { + ok(!rec.isCompleted(), "recording is not yet completed on 'recording-stopping'"); + ok(rec.isFinalizing(), "recording is considered finalizing between 'recording-stopping' and 'recording-stopped'"); + } + + yield stopped; + ok(!rec.isRecording(), "on 'recording-stopped', model is still no longer recording"); + ok(rec.isCompleted(), "on 'recording-stopped', model is considered 'complete'"); + + checkSystemInfo(rec, "Host"); + checkSystemInfo(rec, "Client"); + + // Export and import a rec, and ensure it has the correct state. + let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8)); + yield rec.exportRecording(file); + + let importedModel = yield front.importRecording(file); + + ok(importedModel.isCompleted(), "All imported recordings should be completed"); + ok(!importedModel.isRecording(), "All imported recordings should not be recording"); + ok(importedModel.isImported(), "All imported recordings should be considerd imported"); + + checkSystemInfo(importedModel, "Host"); + checkSystemInfo(importedModel, "Client"); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function checkSystemInfo(recording, type) { + let data = recording[`get${type}SystemInfo`](); + for (let field of ["appid", "apptype", "vendor", "name", "version"]) { + ok(data[field], `get${type}SystemInfo() has ${field} property`); + } +} diff --git a/devtools/server/tests/browser/browser_perf-recording-actor-02.js b/devtools/server/tests/browser/browser_perf-recording-actor-02.js new file mode 100644 index 000000000..8337ad2ef --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-recording-actor-02.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that buffer status is correctly updated in recording models. + */ + +var BUFFER_SIZE = 20000; +var config = { bufferSize: BUFFER_SIZE }; + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + yield front.setProfilerStatusInterval(10); + let model = yield front.startRecording(config); + let stats = yield once(front, "profiler-status"); + is(stats.totalSize, BUFFER_SIZE, `profiler-status event has totalSize: ${stats.totalSize}`); + ok(stats.position < BUFFER_SIZE, `profiler-status event has position: ${stats.position}`); + ok(stats.generation >= 0, `profiler-status event has generation: ${stats.generation}`); + ok(stats.isActive, "profiler-status event is isActive"); + is(typeof stats.currentTime, "number", "profiler-status event has currentTime"); + + // Halt once more for a buffer status to ensure we're beyond 0 + yield once(front, "profiler-status"); + + let lastBufferStatus = 0; + let checkCount = 0; + while (lastBufferStatus < 1) { + let currentBufferStatus = front.getBufferUsageForRecording(model); + ok(currentBufferStatus > lastBufferStatus, `buffer is more filled than before: ${currentBufferStatus} > ${lastBufferStatus}`); + lastBufferStatus = currentBufferStatus; + checkCount++; + yield once(front, "profiler-status"); + } + + ok(checkCount >= 1, "atleast 1 event were fired until the buffer was filled"); + is(lastBufferStatus, 1, "buffer usage cannot surpass 100%"); + yield front.stopRecording(model); + + is(front.getBufferUsageForRecording(model), null, "buffer usage should be null when no longer recording."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-samples-01.js b/devtools/server/tests/browser/browser_perf-samples-01.js new file mode 100644 index 000000000..f8f4bf393 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-samples-01.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the retrieved profiler data samples are correctly filtered and + * normalized before passed to consumers. + */ + +const WAIT_TIME = 1000; // ms + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + // Perform the first recording... + + let firstRecording = yield front.startRecording(); + let firstRecordingStartTime = firstRecording._startTime; + info("Started profiling at: " + firstRecordingStartTime); + + busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity + + yield front.stopRecording(firstRecording); + + ok(firstRecording.getDuration() >= WAIT_TIME, + "The first recording duration is correct."); + + // Perform the second recording... + + let secondRecording = yield front.startRecording(); + let secondRecordingStartTime = secondRecording._startTime; + info("Started profiling at: " + secondRecordingStartTime); + + busyWait(WAIT_TIME); // allow the profiler module to sample more cpu activity + + yield front.stopRecording(secondRecording); + let secondRecordingProfile = secondRecording.getProfile(); + let secondRecordingSamples = secondRecordingProfile.threads[0].samples.data; + + ok(secondRecording.getDuration() >= WAIT_TIME, + "The second recording duration is correct."); + + const TIME_SLOT = secondRecordingProfile.threads[0].samples.schema.time; + ok(secondRecordingSamples[0][TIME_SLOT] < secondRecordingStartTime, + "The second recorded sample times were normalized."); + ok(secondRecordingSamples[0][TIME_SLOT] > 0, + "The second recorded sample times were normalized correctly."); + ok(!secondRecordingSamples.find(e => e[TIME_SLOT] + secondRecordingStartTime <= firstRecording.getDuration()), + "There should be no samples from the first recording in the second one, " + + "even though the total number of frames did not overflow."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-samples-02.js b/devtools/server/tests/browser/browser_perf-samples-02.js new file mode 100644 index 000000000..c4d51230d --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-samples-02.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the retrieved profiler data samples always have a (root) node. + * If this ever changes, the |ThreadNode.prototype.insert| function in + * devtools/client/performance/modules/logic/tree-model.js will have to be changed. + */ + +const WAIT_TIME = 1000; // ms + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let rec = yield front.startRecording(); + busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity + + yield front.stopRecording(rec); + let profile = rec.getProfile(); + let sampleCount = 0; + + for (let thread of profile.threads) { + info("Checking thread: " + thread.name); + + for (let sample of thread.samples.data) { + sampleCount++; + + let stack = getInflatedStackLocations(thread, sample); + if (stack[0] != "(root)") { + ok(false, "The sample " + stack.toSource() + " doesn't have a root node."); + } + } + } + + ok(sampleCount > 0, + "At least some samples have been iterated over, checking for root nodes."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +/** + * Inflate a particular sample's stack and return an array of strings. + */ +function getInflatedStackLocations(thread, sample) { + let stackTable = thread.stackTable; + let frameTable = thread.frameTable; + let stringTable = thread.stringTable; + let SAMPLE_STACK_SLOT = thread.samples.schema.stack; + let STACK_PREFIX_SLOT = stackTable.schema.prefix; + let STACK_FRAME_SLOT = stackTable.schema.frame; + let FRAME_LOCATION_SLOT = frameTable.schema.location; + + // Build the stack from the raw data and accumulate the locations in + // an array. + let stackIndex = sample[SAMPLE_STACK_SLOT]; + let locations = []; + while (stackIndex !== null) { + let stackEntry = stackTable.data[stackIndex]; + let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]]; + locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]); + stackIndex = stackEntry[STACK_PREFIX_SLOT]; + } + + // The profiler tree is inverted, so reverse the array. + return locations.reverse(); +} diff --git a/devtools/server/tests/browser/browser_register_actor.js b/devtools/server/tests/browser/browser_register_actor.js new file mode 100644 index 000000000..73ee0cedc --- /dev/null +++ b/devtools/server/tests/browser/browser_register_actor.js @@ -0,0 +1,76 @@ +var gClient; + +function test() { + waitForExplicitFinish(); + var {ActorRegistryFront} = require("devtools/shared/fronts/actor-registry"); + var actorURL = "chrome://mochitests/content/chrome/devtools/server/tests/mochitest/hello-actor.js"; + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect() + .then(() => gClient.listTabs()) + .then(aResponse => { + + var options = { + prefix: "helloActor", + constructor: "HelloActor", + type: { tab: true } + }; + + var registry = ActorRegistryFront(gClient, aResponse); + registry.registerActor(actorURL, options).then(actorFront => { + gClient.listTabs(response => { + var tab = response.tabs[response.selected]; + ok(!!tab.helloActor, "Hello actor must exist"); + + // Make sure actor's state is maintained across listTabs requests. + checkActorState(tab.helloActor, () => { + + // Clean up + actorFront.unregister().then(() => { + gClient.close().then(() => { + DebuggerServer.destroy(); + gClient = null; + finish(); + }); + }); + }); + }); + }); + }); +} + +function checkActorState(helloActor, callback) { + getCount(helloActor, response => { + ok(!response.error, "No error"); + is(response.count, 1, "The counter must be valid"); + + getCount(helloActor, response => { + ok(!response.error, "No error"); + is(response.count, 2, "The counter must be valid"); + + gClient.listTabs(response => { + var tab = response.tabs[response.selected]; + is(tab.helloActor, helloActor, "Hello actor must be valid"); + + getCount(helloActor, response => { + ok(!response.error, "No error"); + is(response.count, 3, "The counter must be valid"); + + callback(); + }); + }); + }); + }); +} + +function getCount(actor, callback) { + gClient.request({ + to: actor, + type: "count" + }, callback); +} diff --git a/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/devtools/server/tests/browser/browser_storage_dynamic_windows.js new file mode 100644 index 000000000..440c91222 --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js @@ -0,0 +1,294 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {StorageFront} = require("devtools/shared/fronts/storage"); +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this); + +const beforeReload = { + cookies: { + "test1.example.org": ["c1", "cs2", "c3", "uc1"], + "sectest1.example.org": ["uc1", "cs2"] + }, + localStorage: { + "http://test1.example.org": ["ls1", "ls2"], + "http://sectest1.example.org": ["iframe-u-ls1"] + }, + sessionStorage: { + "http://test1.example.org": ["ss1"], + "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"] + }, + indexedDB: { + "http://test1.example.org": [ + JSON.stringify(["idb1", "obj1"]), + JSON.stringify(["idb1", "obj2"]), + JSON.stringify(["idb2", "obj3"]), + ], + "http://sectest1.example.org": [] + } +}; + +function* testStores(data, front) { + testWindowsBeforeReload(data); + + // FIXME: Bug 1183581 - browser_storage_dynamic_windows.js IsSafeToRunScript + // errors when testing reload in E10S mode + // yield testReload(front); + yield testAddIframe(front); + yield testRemoveIframe(front); +} + +function testWindowsBeforeReload(data) { + for (let storageType in beforeReload) { + ok(data[storageType], storageType + " storage actor is present"); + is(Object.keys(data[storageType].hosts).length, + Object.keys(beforeReload[storageType]).length, + "Number of hosts for " + storageType + "match"); + for (let host in beforeReload[storageType]) { + ok(data[storageType].hosts[host], "Host " + host + " is present"); + } + } +} + +function markOutMatched(toBeEmptied, data, deleted) { + if (!Object.keys(toBeEmptied).length) { + info("Object empty"); + return; + } + ok(Object.keys(data).length, + "At least one storage type should be present"); + for (let storageType in toBeEmptied) { + if (!data[storageType]) { + continue; + } + info("Testing for " + storageType); + for (let host in data[storageType]) { + ok(toBeEmptied[storageType][host], "Host " + host + " found"); + if (!deleted) { + for (let item of data[storageType][host]) { + let index = toBeEmptied[storageType][host].indexOf(item); + ok(index > -1, "Item found - " + item); + if (index > -1) { + toBeEmptied[storageType][host].splice(index, 1); + } + } + if (!toBeEmptied[storageType][host].length) { + delete toBeEmptied[storageType][host]; + } + } else { + delete toBeEmptied[storageType][host]; + } + } + if (!Object.keys(toBeEmptied[storageType]).length) { + delete toBeEmptied[storageType]; + } + } +} + +// function testReload(front) { +// info("Testing if reload works properly"); + +// let shouldBeEmptyFirst = Cu.cloneInto(beforeReload, {}); +// let shouldBeEmptyLast = Cu.cloneInto(beforeReload, {}); +// return new Promise(resolve => { + +// let onStoresUpdate = data => { +// info("in stores update of testReload"); +// // This might be second time stores update is happening, in which case, +// // data.deleted will be null. +// // OR.. This might be the first time on a super slow machine where both +// // data.deleted and data.added is missing in the first update. +// if (data.deleted) { +// markOutMatched(shouldBeEmptyFirst, data.deleted, true); +// } + +// if (!Object.keys(shouldBeEmptyFirst).length) { +// info("shouldBeEmptyFirst is empty now"); +// } + +// // stores-update call might not have data.added for the first time on +// // slow machines, in which case, data.added will be null +// if (data.added) { +// markOutMatched(shouldBeEmptyLast, data.added); +// } + +// if (!Object.keys(shouldBeEmptyLast).length) { +// info("Everything to be received is received."); +// endTestReloaded(); +// } +// }; + +// let endTestReloaded = () => { +// front.off("stores-update", onStoresUpdate); +// resolve(); +// }; + +// front.on("stores-update", onStoresUpdate); + +// content.location.reload(); +// }); +// } + +function testAddIframe(front) { + info("Testing if new iframe addition works properly"); + return new Promise(resolve => { + let shouldBeEmpty = { + localStorage: { + "https://sectest1.example.org": ["iframe-s-ls1"] + }, + sessionStorage: { + "https://sectest1.example.org": ["iframe-s-ss1"] + }, + cookies: { + "sectest1.example.org": ["sc1"] + }, + indexedDB: { + // empty because indexed db creation happens after the page load, so at + // the time of window-ready, there was no indexed db present. + "https://sectest1.example.org": [] + }, + Cache: { + "https://sectest1.example.org":[] + } + }; + + let onStoresUpdate = data => { + info("checking if the hosts list is correct for this iframe addition"); + + markOutMatched(shouldBeEmpty, data.added); + + ok(!data.changed || !data.changed.cookies || + !data.changed.cookies["https://sectest1.example.org"], + "Nothing got changed for cookies"); + ok(!data.changed || !data.changed.localStorage || + !data.changed.localStorage["https://sectest1.example.org"], + "Nothing got changed for local storage"); + ok(!data.changed || !data.changed.sessionStorage || + !data.changed.sessionStorage["https://sectest1.example.org"], + "Nothing got changed for session storage"); + ok(!data.changed || !data.changed.indexedDB || + !data.changed.indexedDB["https://sectest1.example.org"], + "Nothing got changed for indexed db"); + + ok(!data.deleted || !data.deleted.cookies || + !data.deleted.cookies["https://sectest1.example.org"], + "Nothing got deleted for cookies"); + ok(!data.deleted || !data.deleted.localStorage || + !data.deleted.localStorage["https://sectest1.example.org"], + "Nothing got deleted for local storage"); + ok(!data.deleted || !data.deleted.sessionStorage || + !data.deleted.sessionStorage["https://sectest1.example.org"], + "Nothing got deleted for session storage"); + ok(!data.deleted || !data.deleted.indexedDB || + !data.deleted.indexedDB["https://sectest1.example.org"], + "Nothing got deleted for indexed db"); + + if (!Object.keys(shouldBeEmpty).length) { + info("Everything to be received is received."); + endTestReloaded(); + } + }; + + let endTestReloaded = () => { + front.off("stores-update", onStoresUpdate); + resolve(); + }; + + front.on("stores-update", onStoresUpdate); + + let iframe = content.document.createElement("iframe"); + iframe.src = ALT_DOMAIN_SECURED + "storage-secured-iframe.html"; + content.document.querySelector("body").appendChild(iframe); + }); +} + +function testRemoveIframe(front) { + info("Testing if iframe removal works properly"); + return new Promise(resolve => { + let shouldBeEmpty = { + localStorage: { + "http://sectest1.example.org": [] + }, + sessionStorage: { + "http://sectest1.example.org": [] + }, + Cache: { + "http://sectest1.example.org": [] + }, + indexedDB: { + "http://sectest1.example.org": [] + } + }; + + let onStoresUpdate = data => { + info("checking if the hosts list is correct for this iframe deletion"); + + markOutMatched(shouldBeEmpty, data.deleted, true); + + ok(!data.deleted.cookies || !data.deleted.cookies["sectest1.example.org"], + "Nothing got deleted for Cookies as " + + "the same hostname is still present"); + + ok(!data.changed || !data.changed.cookies || + !data.changed.cookies["http://sectest1.example.org"], + "Nothing got changed for cookies"); + ok(!data.changed || !data.changed.localStorage || + !data.changed.localStorage["http://sectest1.example.org"], + "Nothing got changed for local storage"); + ok(!data.changed || !data.changed.sessionStorage || + !data.changed.sessionStorage["http://sectest1.example.org"], + "Nothing got changed for session storage"); + + ok(!data.added || !data.added.cookies || + !data.added.cookies["http://sectest1.example.org"], + "Nothing got added for cookies"); + ok(!data.added || !data.added.localStorage || + !data.added.localStorage["http://sectest1.example.org"], + "Nothing got added for local storage"); + ok(!data.added || !data.added.sessionStorage || + !data.added.sessionStorage["http://sectest1.example.org"], + "Nothing got added for session storage"); + + if (!Object.keys(shouldBeEmpty).length) { + info("Everything to be received is received."); + endTestReloaded(); + } + }; + + let endTestReloaded = () => { + front.off("stores-update", onStoresUpdate); + resolve(); + }; + + front.on("stores-update", onStoresUpdate); + + for (let iframe of content.document.querySelectorAll("iframe")) { + if (iframe.src.startsWith("http:")) { + iframe.remove(); + break; + } + } + }); +} + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-dynamic-windows.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let data = yield front.listStores(); + yield testStores(data, front); + + yield clearStorage(); + + // Forcing GC/CC to get rid of docshells and windows created by this test. + forceCollections(); + yield client.close(); + forceCollections(); + DebuggerServer.destroy(); + forceCollections(); +}); diff --git a/devtools/server/tests/browser/browser_storage_listings.js b/devtools/server/tests/browser/browser_storage_listings.js new file mode 100644 index 000000000..4ff3c3fc1 --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_listings.js @@ -0,0 +1,610 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {StorageFront} = require("devtools/shared/fronts/storage"); +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this); + +const storeMap = { + cookies: { + "test1.example.org": [ + { + name: "c1", + value: "foobar", + expires: 2000000000000, + path: "/browser", + host: "test1.example.org", + isDomain: false, + isSecure: false, + }, + { + name: "cs2", + value: "sessionCookie", + path: "/", + host: ".example.org", + expires: 0, + isDomain: true, + isSecure: false, + }, + { + name: "c3", + value: "foobar-2", + expires: 2000000001000, + path: "/", + host: "test1.example.org", + isDomain: false, + isSecure: false, + }, + { + name: "uc1", + value: "foobar", + host: ".example.org", + path: "/", + expires: 0, + isDomain: true, + isSecure: true, + } + ], + "sectest1.example.org": [ + { + name: "uc1", + value: "foobar", + host: ".example.org", + path: "/", + expires: 0, + isDomain: true, + isSecure: true, + }, + { + name: "cs2", + value: "sessionCookie", + path: "/", + host: ".example.org", + expires: 0, + isDomain: true, + isSecure: false, + }, + { + name: "sc1", + value: "foobar", + path: "/browser/devtools/server/tests/browser/", + host: "sectest1.example.org", + expires: 0, + isDomain: false, + isSecure: false, + } + ] + }, + localStorage: { + "http://test1.example.org": [ + { + name: "ls1", + value: "foobar" + }, + { + name: "ls2", + value: "foobar-2" + } + ], + "http://sectest1.example.org": [ + { + name: "iframe-u-ls1", + value: "foobar" + } + ], + "https://sectest1.example.org": [ + { + name: "iframe-s-ls1", + value: "foobar" + } + ] + }, + sessionStorage: { + "http://test1.example.org": [ + { + name: "ss1", + value: "foobar-3" + } + ], + "http://sectest1.example.org": [ + { + name: "iframe-u-ss1", + value: "foobar1" + }, + { + name: "iframe-u-ss2", + value: "foobar2" + } + ], + "https://sectest1.example.org": [ + { + name: "iframe-s-ss1", + value: "foobar-2" + } + ] + } +}; + +const IDBValues = { + listStoresResponse: { + "http://test1.example.org": [ + ["idb1", "obj1"], ["idb1", "obj2"], ["idb2", "obj3"] + ], + "http://sectest1.example.org": [ + ], + "https://sectest1.example.org": [ + ["idb-s1", "obj-s1"], ["idb-s2", "obj-s2"] + ] + }, + dbDetails : { + "http://test1.example.org": [ + { + db: "idb1", + origin: "http://test1.example.org", + version: 1, + objectStores: 2 + }, + { + db: "idb2", + origin: "http://test1.example.org", + version: 1, + objectStores: 1 + }, + ], + "http://sectest1.example.org": [ + ], + "https://sectest1.example.org": [ + { + db: "idb-s1", + origin: "https://sectest1.example.org", + version: 1, + objectStores: 1 + }, + { + db: "idb-s2", + origin: "https://sectest1.example.org", + version: 1, + objectStores: 1 + }, + ] + }, + objectStoreDetails: { + "http://test1.example.org": { + idb1: [ + { + objectStore: "obj1", + keyPath: "id", + autoIncrement: false, + indexes: [ + { + name: "name", + keyPath: "name", + "unique": false, + multiEntry: false, + }, + { + name: "email", + keyPath: "email", + "unique": true, + multiEntry: false, + }, + ] + }, + { + objectStore: "obj2", + keyPath: "id2", + autoIncrement: false, + indexes: [] + } + ], + idb2: [ + { + objectStore: "obj3", + keyPath: "id3", + autoIncrement: false, + indexes: [ + { + name: "name2", + keyPath: "name2", + "unique": true, + multiEntry: false, + } + ] + }, + ] + }, + "http://sectest1.example.org" : {}, + "https://sectest1.example.org": { + "idb-s1": [ + { + objectStore: "obj-s1", + keyPath: "id", + autoIncrement: false, + indexes: [] + }, + ], + "idb-s2": [ + { + objectStore: "obj-s2", + keyPath: "id3", + autoIncrement: true, + indexes: [ + { + name: "name2", + keyPath: "name2", + "unique": true, + multiEntry: false, + } + ] + }, + ] + } + + }, + entries: { + "http://test1.example.org": { + "idb1#obj1": [ + { + name: 1, + value: { + id: 1, + name: "foo", + email: "foo@bar.com", + } + }, + { + name: 2, + value: { + id: 2, + name: "foo2", + email: "foo2@bar.com", + } + }, + { + name: 3, + value: { + id: 3, + name: "foo2", + email: "foo3@bar.com", + } + } + ], + "idb1#obj2": [ + { + name: 1, + value: { + id2: 1, + name: "foo", + email: "foo@bar.com", + extra: "baz" + } + } + ], + "idb2#obj3": [] + }, + "http://sectest1.example.org" : {}, + "https://sectest1.example.org": { + "idb-s1#obj-s1": [ + { + name: 6, + value: { + id: 6, + name: "foo", + email: "foo@bar.com", + } + }, + { + name: 7, + value: { + id: 7, + name: "foo2", + email: "foo2@bar.com", + } + } + ], + "idb-s2#obj-s2": [ + { + name: 13, + value: { + id2: 13, + name2: "foo", + email: "foo@bar.com", + } + } + ] + } + } +}; + +function finishTests(client) { + + let closeConnection = () => { + + }; +} + +function* testStores(data) { + ok(data.cookies, "Cookies storage actor is present"); + ok(data.localStorage, "Local Storage storage actor is present"); + ok(data.sessionStorage, "Session Storage storage actor is present"); + ok(data.indexedDB, "Indexed DB storage actor is present"); + yield testCookies(data.cookies); + yield testLocalStorage(data.localStorage); + yield testSessionStorage(data.sessionStorage); + yield testIndexedDB(data.indexedDB); +} + +function testCookies(cookiesActor) { + is(Object.keys(cookiesActor.hosts).length, 2, "Correct number of host entries for cookies"); + return testCookiesObjects(0, cookiesActor.hosts, cookiesActor); +} + +var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + let cookiesLength = 0; + for (let secureCookie of storeMap.cookies[host]) { + if (secureCookie.isSecure) { + ++cookiesLength; + } + } + // Any secure cookies did not get stored in the database. + is(data.total, storeMap.cookies[host].length - cookiesLength, + "Number of cookies in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of storeMap.cookies[host]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found cookie " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + is(item.expires, toMatch.expires, "The expiry time matches."); + is(item.path, toMatch.path, "The path matches."); + is(item.host, toMatch.host, "The host matches."); + is(item.isSecure, toMatch.isSecure, "The isSecure value matches."); + is(item.isDomain, toMatch.isDomain, "The isDomain value matches."); + break; + } + } + ok(found, "cookie " + item.name + " should exist in response"); + } + }; + + ok(!!storeMap.cookies[host], "Host is present in the list : " + host); + matchItems(yield cookiesActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testCookiesObjects(++index, hosts, cookiesActor); +}); + +function testLocalStorage(localStorageActor) { + is(Object.keys(localStorageActor.hosts).length, 3, + "Correct number of host entries for local storage"); + return testLocalStorageObjects(0, localStorageActor.hosts, localStorageActor); +} + +var testLocalStorageObjects = Task.async(function* (index, hosts, localStorageActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, storeMap.localStorage[host].length, + "Number of local storage items in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of storeMap.localStorage[host]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found local storage item " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + break; + } + } + ok(found, "local storage item " + item.name + " should exist in response"); + } + }; + + ok(!!storeMap.localStorage[host], "Host is present in the list : " + host); + matchItems(yield localStorageActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testLocalStorageObjects(++index, hosts, localStorageActor); +}); + +function testSessionStorage(sessionStorageActor) { + is(Object.keys(sessionStorageActor.hosts).length, 3, + "Correct number of host entries for session storage"); + return testSessionStorageObjects(0, sessionStorageActor.hosts, + sessionStorageActor); +} + +var testSessionStorageObjects = Task.async(function* (index, hosts, sessionStorageActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, storeMap.sessionStorage[host].length, + "Number of session storage items in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of storeMap.sessionStorage[host]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found session storage item " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + break; + } + } + ok(found, "session storage item " + item.name + " should exist in response"); + } + }; + + ok(!!storeMap.sessionStorage[host], "Host is present in the list : " + host); + matchItems(yield sessionStorageActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testSessionStorageObjects(++index, hosts, sessionStorageActor); +}); + +var testIndexedDB = Task.async(function* (indexedDBActor) { + is(Object.keys(indexedDBActor.hosts).length, 3, + "Correct number of host entries for indexed db"); + + for (let host in indexedDBActor.hosts) { + for (let item of indexedDBActor.hosts[host]) { + let parsedItem = JSON.parse(item); + let found = false; + for (let toMatch of IDBValues.listStoresResponse[host]) { + if (toMatch[0] == parsedItem[0] && toMatch[1] == parsedItem[1]) { + found = true; + break; + } + } + ok(found, item + " should exist in list stores response"); + } + } + + yield testIndexedDBs(0, indexedDBActor.hosts, indexedDBActor); + yield testObjectStores(0, indexedDBActor.hosts, indexedDBActor); + yield testIDBEntries(0, indexedDBActor.hosts, indexedDBActor); +}); + +var testIndexedDBs = Task.async(function* (index, hosts, indexedDBActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, IDBValues.dbDetails[host].length, + "Number of indexed db in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of IDBValues.dbDetails[host]) { + if (item.db == toMatch.db) { + found = true; + ok(true, "Found indexed db " + item.db + " in response"); + is(item.origin, toMatch.origin, "The origin matches."); + is(item.version, toMatch.version, "The version matches."); + is(item.objectStores, toMatch.objectStores, + "The numebr of object stores matches."); + break; + } + } + ok(found, "indexed db " + item.name + " should exist in response"); + } + }; + + ok(!!IDBValues.dbDetails[host], "Host is present in the list : " + host); + matchItems(yield indexedDBActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testIndexedDBs(++index, hosts, indexedDBActor); +}); + +var testObjectStores = Task.async(function* (index, hosts, indexedDBActor) { + let host = Object.keys(hosts)[index]; + let matchItems = (data, db) => { + is(data.total, IDBValues.objectStoreDetails[host][db].length, + "Number of object stores in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of IDBValues.objectStoreDetails[host][db]) { + if (item.objectStore == toMatch.objectStore) { + found = true; + ok(true, "Found object store " + item.objectStore + " in response"); + is(item.keyPath, toMatch.keyPath, "The keyPath matches."); + is(item.autoIncrement, toMatch.autoIncrement, "The autoIncrement matches."); + item.indexes = JSON.parse(item.indexes); + is(item.indexes.length, toMatch.indexes.length, "Number of indexes match"); + for (let index of item.indexes) { + let indexFound = false; + for (let toMatchIndex of toMatch.indexes) { + if (toMatchIndex.name == index.name) { + indexFound = true; + ok(true, "Found index " + index.name); + is(index.keyPath, toMatchIndex.keyPath, + "The keyPath of index matches."); + is(index.unique, toMatchIndex.unique, "The unique matches"); + is(index.multiEntry, toMatchIndex.multiEntry, + "The multiEntry matches"); + break; + } + } + ok(indexFound, "Index " + index + " should exist in response"); + } + break; + } + } + ok(found, "indexed db " + item.name + " should exist in response"); + } + }; + + ok(!!IDBValues.objectStoreDetails[host], "Host is present in the list : " + host); + for (let name of hosts[host]) { + let objName = JSON.parse(name).slice(0, 1); + matchItems(( + yield indexedDBActor.getStoreObjects(host, [JSON.stringify(objName)]) + ), objName[0]); + } + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testObjectStores(++index, hosts, indexedDBActor); +}); + +var testIDBEntries = Task.async(function* (index, hosts, indexedDBActor) { + let host = Object.keys(hosts)[index]; + let matchItems = (data, obj) => { + is(data.total, IDBValues.entries[host][obj].length, + "Number of items in object store " + obj + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of IDBValues.entries[host][obj]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found indexed db item " + item.name + " in response"); + let value = JSON.parse(item.value.str); + is(Object.keys(value).length, Object.keys(toMatch.value).length, + "Number of entries in the value matches"); + for (let key in value) { + is(value[key], toMatch.value[key], + "value of " + key + " value key matches"); + } + break; + } + } + ok(found, "indexed db item " + item.name + " should exist in response"); + } + }; + + ok(!!IDBValues.entries[host], "Host is present in the list : " + host); + for (let name of hosts[host]) { + let parsed = JSON.parse(name); + matchItems(( + yield indexedDBActor.getStoreObjects(host, [name]) + ), parsed[0] + "#" + parsed[1]); + } + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testObjectStores(++index, hosts, indexedDBActor); +}); + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let data = yield front.listStores(); + yield testStores(data); + + yield clearStorage(); + + // Forcing GC/CC to get rid of docshells and windows created by this test. + forceCollections(); + yield client.close(); + forceCollections(); + DebuggerServer.destroy(); + forceCollections(); +}); diff --git a/devtools/server/tests/browser/browser_storage_updates.js b/devtools/server/tests/browser/browser_storage_updates.js new file mode 100644 index 000000000..28b2e509f --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_updates.js @@ -0,0 +1,304 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {StorageFront} = require("devtools/shared/fronts/storage"); +const beforeReload = { + cookies: ["test1.example.org", "sectest1.example.org"], + localStorage: ["http://test1.example.org", "http://sectest1.example.org"], + sessionStorage: ["http://test1.example.org", "http://sectest1.example.org"], +}; + +const TESTS = [ + // index 0 + { + action: function (win) { + info('win.addCookie("c1", "foobar1")'); + win.addCookie("c1", "foobar1"); + + info('win.addCookie("c2", "foobar2")'); + win.addCookie("c2", "foobar2"); + + info('win.localStorage.setItem("l1", "foobar1")'); + win.localStorage.setItem("l1", "foobar1"); + }, + expected: { + added: { + cookies: { + "test1.example.org": ["c1", "c2"] + }, + localStorage: { + "http://test1.example.org": ["l1"] + } + } + } + }, + + // index 1 + { + action: function (win) { + info('win.addCookie("c1", "new_foobar1")'); + win.addCookie("c1", "new_foobar1"); + + info('win.localStorage.setItem("l2", "foobar2")'); + win.localStorage.setItem("l2", "foobar2"); + }, + expected: { + changed: { + cookies: { + "test1.example.org": ["c1"] + } + }, + added: { + localStorage: { + "http://test1.example.org": ["l2"] + } + } + } + }, + + // index 2 + { + action: function (win) { + info('win.removeCookie("c2")'); + win.removeCookie("c2"); + + info('win.localStorage.removeItem("l1")'); + win.localStorage.removeItem("l1"); + + info('win.localStorage.setItem("l3", "foobar3")'); + win.localStorage.setItem("l3", "foobar3"); + }, + expected: { + deleted: { + cookies: { + "test1.example.org": ["c2"] + }, + localStorage: { + "http://test1.example.org": ["l1"] + } + }, + added: { + localStorage: { + "http://test1.example.org": ["l3"] + } + } + } + }, + + // index 3 + { + action: function (win) { + info('win.removeCookie("c1")'); + win.removeCookie("c1"); + + info('win.addCookie("c3", "foobar3")'); + win.addCookie("c3", "foobar3"); + + info('win.localStorage.removeItem("l2")'); + win.localStorage.removeItem("l2"); + + info('win.sessionStorage.setItem("s1", "foobar1")'); + win.sessionStorage.setItem("s1", "foobar1"); + + info('win.sessionStorage.setItem("s2", "foobar2")'); + win.sessionStorage.setItem("s2", "foobar2"); + + info('win.localStorage.setItem("l3", "new_foobar3")'); + win.localStorage.setItem("l3", "new_foobar3"); + }, + expected: { + added: { + cookies: { + "test1.example.org": ["c3"] + }, + sessionStorage: { + "http://test1.example.org": ["s1", "s2"] + } + }, + changed: { + localStorage: { + "http://test1.example.org": ["l3"] + } + }, + deleted: { + cookies: { + "test1.example.org": ["c1"] + }, + localStorage: { + "http://test1.example.org": ["l2"] + } + } + } + }, + + // index 4 + { + action: function (win) { + info('win.sessionStorage.removeItem("s1")'); + win.sessionStorage.removeItem("s1"); + }, + expected: { + deleted: { + sessionStorage: { + "http://test1.example.org": ["s1"] + } + } + } + }, + + // index 5 + { + action: function (win) { + info("win.clearCookies()"); + win.clearCookies(); + }, + expected: { + deleted: { + cookies: { + "test1.example.org": ["c3"] + } + } + } + } +]; + +function markOutMatched(toBeEmptied, data) { + if (!Object.keys(toBeEmptied).length) { + info("Object empty"); + return; + } + ok(Object.keys(data).length, "At least one storage type should be present"); + + for (let storageType in toBeEmptied) { + if (!data[storageType]) { + continue; + } + info("Testing for " + storageType); + for (let host in data[storageType]) { + ok(toBeEmptied[storageType][host], "Host " + host + " found"); + + for (let item of data[storageType][host]) { + let index = toBeEmptied[storageType][host].indexOf(item); + ok(index > -1, "Item found - " + item); + if (index > -1) { + toBeEmptied[storageType][host].splice(index, 1); + } + } + if (!toBeEmptied[storageType][host].length) { + delete toBeEmptied[storageType][host]; + } + } + if (!Object.keys(toBeEmptied[storageType]).length) { + delete toBeEmptied[storageType]; + } + } +} + +function onStoresUpdate(expected, {added, changed, deleted}, index) { + info("inside stores update for index " + index); + + // Here, added, changed and deleted might be null even if they are required as + // per expected. This is fine as they might come in the next stores-update + // call or have already come in the previous one. + if (added) { + info("matching added object for index " + index); + markOutMatched(expected.added, added); + } + if (changed) { + info("matching changed object for index " + index); + markOutMatched(expected.changed, changed); + } + if (deleted) { + info("matching deleted object for index " + index); + markOutMatched(expected.deleted, deleted); + } + + if ((!expected.added || !Object.keys(expected.added).length) && + (!expected.changed || !Object.keys(expected.changed).length) && + (!expected.deleted || !Object.keys(expected.deleted).length)) { + info("Everything expected has been received for index " + index); + } else { + info("Still some updates pending for index " + index); + } +} + +function runTest({action, expected}, front, win, index) { + return new Promise(resolve => { + front.once("stores-update", function (addedChangedDeleted) { + onStoresUpdate(expected, addedChangedDeleted, index); + resolve(); + }); + + info("Running test at index " + index); + action(win); + }); +} + +function* testClearLocalAndSessionStores(front, win) { + return new Promise(resolve => { + // We need to wait until we have received stores-cleared for both local and + // session storage. + let localStorage = false; + let sessionStorage = false; + + front.on("stores-cleared", function onStoresCleared(data) { + storesCleared(data); + + if (data.localStorage) { + localStorage = true; + } + if (data.sessionStorage) { + sessionStorage = true; + } + if (localStorage && sessionStorage) { + front.off("stores-cleared", onStoresCleared); + resolve(); + } + }); + + win.clearLocalAndSessionStores(); + }); +} + +function storesCleared(data) { + if (data.sessionStorage || data.localStorage) { + let hosts = data.sessionStorage || data.localStorage; + info("Stores cleared required for session storage"); + is(hosts.length, 1, "number of hosts is 1"); + is(hosts[0], "http://test1.example.org", + "host matches for " + Object.keys(data)[0]); + } else { + ok(false, "Stores cleared should only be for local and session storage"); + } +} + +function* finishTests(client) { + yield client.close(); + DebuggerServer.destroy(); + finish(); +} + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "storage-updates.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let win = doc.defaultView.wrappedJSObject; + + yield front.listStores(); + + for (let i = 0; i < TESTS.length; i++) { + let test = TESTS[i]; + yield runTest(test, front, win, i); + } + + yield testClearLocalAndSessionStores(front, win); + yield finishTests(client); +}); diff --git a/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js new file mode 100644 index 000000000..f7bb7057e --- /dev/null +++ b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js @@ -0,0 +1,40 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that StyleSheetActor.getText handles empty text correctly. + +const {StyleSheetsFront} = require("devtools/shared/fronts/stylesheets"); + +const CONTENT = ""; +const TEST_URI = "data:text/html;charset=utf-8," + encodeURIComponent(CONTENT); + +add_task(function* () { + yield addTab(TEST_URI); + + info("Initialising the debugger server and client."); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + info("Attaching to the active tab."); + yield client.attachTab(form.actor); + + let front = StyleSheetsFront(client, form); + ok(front, "The StyleSheetsFront was created."); + + let sheets = yield front.getStyleSheets(); + ok(sheets, "getStyleSheets() succeeded"); + is(sheets.length, 1, + "getStyleSheets() returned the correct number of sheets"); + + let sheet = sheets[0]; + yield sheet.update("", false); + let longStr = yield sheet.getText(); + let source = yield longStr.string(); + is(source, "", "text is empty"); + + yield client.close(); +}); diff --git a/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js b/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js new file mode 100644 index 000000000..c382b6ce8 --- /dev/null +++ b/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js @@ -0,0 +1,38 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that StyleSheetsActor.getStyleSheets() works if an iframe does not have +// a content document. + +const {StyleSheetsFront} = require("devtools/shared/fronts/stylesheets"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "stylesheets-nested-iframes.html"); + let doc = browser.contentDocument; + + info("Initialising the debugger server and client."); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + info("Attaching to the active tab."); + yield client.attachTab(form.actor); + + let front = StyleSheetsFront(client, form); + ok(front, "The StyleSheetsFront was created."); + + let sheets = yield front.getStyleSheets(); + ok(sheets, "getStyleSheets() succeeded even with documentless iframes."); + + // Bug 285395 limits the number of nested iframes to 10. There's one sheet per + // frame so we should get 10 sheets. However, the limit might change in the + // future so it's better not to rely on the limit. Asserting > 2 ensures that + // the test page is actually loading nested iframes and this test is doing + // something sensible (if we got this far, the test has served its purpose). + ok(sheets.length > 2, sheets.length + " sheets found (expected 3 or more)."); + + yield client.close(); +}); diff --git a/devtools/server/tests/browser/browser_timeline.js b/devtools/server/tests/browser/browser_timeline.js new file mode 100644 index 000000000..1e5793447 --- /dev/null +++ b/devtools/server/tests/browser/browser_timeline.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the timeline front's start/stop/isRecording methods work in a +// simple use case, and that markers events are sent when operations occur. +// Note that this test isn't concerned with which markers are actually recorded, +// just that markers are recorded at all. +// Trying to check marker types here may lead to intermittents, see bug 1066474. + +const {TimelineFront} = require("devtools/shared/fronts/timeline"); + +add_task(function* () { + let browser = yield addTab("data:text/html;charset=utf-8,mop"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + + ok(front, "The TimelineFront was created"); + + let isActive = yield front.isRecording(); + ok(!isActive, "The TimelineFront is not initially recording"); + + info("Flush any pending reflows"); + let forceSyncReflow = doc.body.innerHeight; + + info("Start recording"); + yield front.start({ withMarkers: true }); + + isActive = yield front.isRecording(); + ok(isActive, "The TimelineFront is now recording"); + + info("Change some style on the page to cause style/reflow/paint"); + let onMarkers = once(front, "markers"); + doc.body.style.padding = "10px"; + let markers = yield onMarkers; + + ok(true, "The markers event was fired"); + ok(markers.length > 0, "Markers were returned"); + + info("Flush pending reflows again"); + forceSyncReflow = doc.body.innerHeight; + + info("Change some style on the page to cause style/paint"); + onMarkers = once(front, "markers"); + doc.body.style.backgroundColor = "red"; + markers = yield onMarkers; + + ok(markers.length > 0, "markers were returned"); + + yield front.stop(); + + isActive = yield front.isRecording(); + ok(!isActive, "Not recording after stop()"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_timeline_actors.js b/devtools/server/tests/browser/browser_timeline_actors.js new file mode 100644 index 000000000..a902775fa --- /dev/null +++ b/devtools/server/tests/browser/browser_timeline_actors.js @@ -0,0 +1,69 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the timeline can also record data from the memory and framerate +// actors, emitted as events in tadem with the markers. + +const {TimelineFront} = require("devtools/shared/fronts/timeline"); + +add_task(function* () { + let browser = yield addTab("data:text/html;charset=utf-8,mop"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + + info("Start timeline marker recording"); + yield front.start({ withMemory: true, withTicks: true }); + + let updatedMemory = 0; + let updatedTicks = 0; + + front.on("memory", (delta, measurement) => { + ok(delta > 0, "The delta should be a timestamp."); + ok(measurement, "The measurement should not be null."); + ok(measurement.total > 0, "There should be a 'total' value in the measurement."); + info("Received 'memory' event at " + delta + " with " + measurement.toSource()); + updatedMemory++; + }); + + front.on("ticks", (delta, ticks) => { + ok(delta > 0, "The delta should be a timestamp."); + ok(ticks, "The ticks should not be null."); + info("Received 'ticks' event with " + ticks.toSource()); + updatedTicks++; + }); + + ok((yield waitUntil(() => updatedMemory > 1)), + "Some memory measurements were emitted."); + ok((yield waitUntil(() => updatedTicks > 1)), + "Some refresh driver ticks were emitted."); + + info("Stop timeline marker recording"); + yield front.stop(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => + setTimeout(function () { + waitUntil(predicate).then(() => resolve(true)); + }, interval)); +} diff --git a/devtools/server/tests/browser/browser_timeline_iframes.js b/devtools/server/tests/browser/browser_timeline_iframes.js new file mode 100644 index 000000000..60728873f --- /dev/null +++ b/devtools/server/tests/browser/browser_timeline_iframes.js @@ -0,0 +1,41 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the timeline front receives markers events for operations that occur in +// iframes. + +const {TimelineFront} = require("devtools/shared/fronts/timeline"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + + info("Start timeline marker recording"); + yield front.start({ withMarkers: true }); + + // Check that we get markers for a few iterations of the timer that runs in + // the child frame. + for (let i = 0; i < 3; i++) { + yield wait(300); // That's the time the child frame waits before changing styles. + let markers = yield once(front, "markers"); + ok(markers.length, "Markers were received for operations in the child frame"); + } + + info("Stop timeline marker recording"); + yield front.stop(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function wait(ms) { + return new Promise(resolve => + setTimeout(resolve, ms)); +} diff --git a/devtools/server/tests/browser/director-script-target.html b/devtools/server/tests/browser/director-script-target.html new file mode 100644 index 000000000..0b0b56d64 --- /dev/null +++ b/devtools/server/tests/browser/director-script-target.html @@ -0,0 +1,15 @@ + + + + + +

debug script target

+ + diff --git a/devtools/server/tests/browser/doc_allocations.html b/devtools/server/tests/browser/doc_allocations.html new file mode 100644 index 000000000..314c40cac --- /dev/null +++ b/devtools/server/tests/browser/doc_allocations.html @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/devtools/server/tests/browser/doc_force_cc.html b/devtools/server/tests/browser/doc_force_cc.html new file mode 100644 index 000000000..d5868bd6b --- /dev/null +++ b/devtools/server/tests/browser/doc_force_cc.html @@ -0,0 +1,29 @@ + + + + + + + Performance tool + cycle collection test page + + + + + + + diff --git a/devtools/server/tests/browser/doc_force_gc.html b/devtools/server/tests/browser/doc_force_gc.html new file mode 100644 index 000000000..f8b617533 --- /dev/null +++ b/devtools/server/tests/browser/doc_force_gc.html @@ -0,0 +1,27 @@ + + + + + + + Performance tool + garbage collection test page + + + + + + + diff --git a/devtools/server/tests/browser/doc_innerHTML.html b/devtools/server/tests/browser/doc_innerHTML.html new file mode 100644 index 000000000..f5ce72de2 --- /dev/null +++ b/devtools/server/tests/browser/doc_innerHTML.html @@ -0,0 +1,21 @@ + + + + + + + Performance tool + innerHTML test page + + + + + + + diff --git a/devtools/server/tests/browser/doc_perf.html b/devtools/server/tests/browser/doc_perf.html new file mode 100644 index 000000000..1da36328b --- /dev/null +++ b/devtools/server/tests/browser/doc_perf.html @@ -0,0 +1,25 @@ + + + + + + + Performance test page + + + + + + + diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js new file mode 100644 index 000000000..1e7f09d95 --- /dev/null +++ b/devtools/server/tests/browser/head.js @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +const {console} = Cu.import("resource://gre/modules/Console.jsm", {}); +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {DebuggerClient} = require("devtools/shared/client/main"); +const {DebuggerServer} = require("devtools/server/main"); +const {defer} = require("promise"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const Services = require("Services"); + +const PATH = "browser/devtools/server/tests/browser/"; +const MAIN_DOMAIN = "http://test1.example.org/" + PATH; +const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; +const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; + +// All tests are asynchronous. +waitForExplicitFinish(); + +/** + * Add a new test tab in the browser and load the given url. + * @param {String} url The url to be loaded in the new tab + * @return a promise that resolves to the new browser that the document + * is loaded in. Note that we cannot return the document + * directly, since this would be a CPOW in the e10s case, + * and Promises cannot be resolved with CPOWs (see bug 1233497). + */ +var addTab = Task.async(function* (url) { + info(`Adding a new tab with URL: ${url}`); + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info(`Tab added and URL ${url} loaded`); + + return tab.linkedBrowser; +}); + +function* initAnimationsFrontForUrl(url) { + const {AnimationsFront} = require("devtools/shared/fronts/animation"); + const {InspectorFront} = require("devtools/shared/fronts/inspector"); + + yield addTab(url); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let inspector = InspectorFront(client, form); + let walker = yield inspector.getWalker(); + let animations = AnimationsFront(client, form); + + return {inspector, walker, animations, client}; +} + +function initDebuggerServer() { + try { + // Sometimes debugger server does not get destroyed correctly by previous + // tests. + DebuggerServer.destroy(); + } catch (e) { + info(`DebuggerServer destroy error: ${e}\n${e.stack}`); + } + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); +} + +/** + * Connect a debugger client. + * @param {DebuggerClient} + * @return {Promise} Resolves to the selected tabActor form when the client is + * connected. + */ +function connectDebuggerClient(client) { + return client.connect() + .then(() => client.listTabs()) + .then(tabs => { + return tabs.tabs[tabs.selected]; + }); +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture = false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + return new Promise(resolve => { + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + info("Got event: '" + eventName + "' on " + target + "."); + target[remove](eventName, onEvent, useCapture); + resolve(...aArgs); + }, useCapture); + break; + } + } + }); +} + +/** + * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and + * windows. + */ +function forceCollections() { + Cu.forceGC(); + Cu.forceCC(); + Cu.forceShrinkingGC(); +} + +/** + * Get a mock tabActor from a given window. + * This is sometimes useful to test actors or classes that use the tabActor in + * isolation. + * @param {DOMWindow} win + * @return {Object} + */ +function getMockTabActor(win) { + return { + window: win, + isRootActor: true + }; +} + +registerCleanupFunction(function tearDown() { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +function idleWait(time) { + return DevToolsUtils.waitForTime(time); +} + +function busyWait(time) { + let start = Date.now(); + let stack; + while (Date.now() - start < time) { stack = Components.stack; } +} + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => { + setTimeout(function () { + waitUntil(predicate).then(() => resolve(true)); + }, interval); + }); +} + +function waitForMarkerType(front, types, predicate, + unpackFun = (name, data) => data.markers, + eventName = "timeline-data") +{ + types = [].concat(types); + predicate = predicate || function () { return true; }; + let filteredMarkers = []; + let { promise, resolve } = defer(); + + info("Waiting for markers of type: " + types); + + function handler(name, data) { + if (typeof name === "string" && name !== "markers") { + return; + } + + let markers = unpackFun(name, data); + info("Got markers: " + JSON.stringify(markers, null, 2)); + + filteredMarkers = filteredMarkers.concat(markers.filter(m => types.indexOf(m.name) !== -1)); + + if (types.every(t => filteredMarkers.some(m => m.name === t)) && predicate(filteredMarkers)) { + front.off(eventName, handler); + resolve(filteredMarkers); + } + } + front.on(eventName, handler); + + return promise; +} diff --git a/devtools/server/tests/browser/navigate-first.html b/devtools/server/tests/browser/navigate-first.html new file mode 100644 index 000000000..829372427 --- /dev/null +++ b/devtools/server/tests/browser/navigate-first.html @@ -0,0 +1,15 @@ + + + + + + +First + + + diff --git a/devtools/server/tests/browser/navigate-second.html b/devtools/server/tests/browser/navigate-second.html new file mode 100644 index 000000000..4b30fe465 --- /dev/null +++ b/devtools/server/tests/browser/navigate-second.html @@ -0,0 +1,9 @@ + + + + + + +Second + + diff --git a/devtools/server/tests/browser/storage-dynamic-windows.html b/devtools/server/tests/browser/storage-dynamic-windows.html new file mode 100644 index 000000000..67aa35d67 --- /dev/null +++ b/devtools/server/tests/browser/storage-dynamic-windows.html @@ -0,0 +1,117 @@ + + + + + + Storage inspector test for listing hosts and storages + + + + + + diff --git a/devtools/server/tests/browser/storage-helpers.js b/devtools/server/tests/browser/storage-helpers.js new file mode 100644 index 000000000..1c4f37705 --- /dev/null +++ b/devtools/server/tests/browser/storage-helpers.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This generator function opens the given url in a new tab, then sets up the + * page by waiting for all cookies, indexedDB items etc. to be created. + * + * @param url {String} The url to be opened in the new tab + * + * @return {Promise} A promise that resolves after storage inspector is ready + */ +function* openTabAndSetupStorage(url) { + let content = yield addTab(url); + + // Setup the async storages in main window and for all its iframes + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + /** + * Get all windows including frames recursively. + * + * @param {Window} [baseWindow] + * The base window at which to start looking for child windows + * (optional). + * @return {Set} + * A set of windows. + */ + function getAllWindows(baseWindow) { + let windows = new Set(); + + let _getAllWindows = function (win) { + windows.add(win.wrappedJSObject); + + for (let i = 0; i < win.length; i++) { + _getAllWindows(win[i]); + } + }; + _getAllWindows(baseWindow); + + return windows; + } + + let windows = getAllWindows(content); + for (let win of windows) { + if (win.setup) { + yield win.setup(); + } + } + }); +} + +function* clearStorage() { + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + /** + * Get all windows including frames recursively. + * + * @param {Window} [baseWindow] + * The base window at which to start looking for child windows + * (optional). + * @return {Set} + * A set of windows. + */ + function getAllWindows(baseWindow) { + let windows = new Set(); + + let _getAllWindows = function (win) { + windows.add(win.wrappedJSObject); + + for (let i = 0; i < win.length; i++) { + _getAllWindows(win[i]); + } + }; + _getAllWindows(baseWindow); + + return windows; + } + + let windows = getAllWindows(content); + for (let win of windows) { + if (win.clear) { + yield win.clear(); + } + } + }); +} diff --git a/devtools/server/tests/browser/storage-listings.html b/devtools/server/tests/browser/storage-listings.html new file mode 100644 index 000000000..c3b7ef3c8 --- /dev/null +++ b/devtools/server/tests/browser/storage-listings.html @@ -0,0 +1,123 @@ + + + + + + Storage inspector test for listing hosts and storages + + + + + + + diff --git a/devtools/server/tests/browser/storage-secured-iframe.html b/devtools/server/tests/browser/storage-secured-iframe.html new file mode 100644 index 000000000..860b20aab --- /dev/null +++ b/devtools/server/tests/browser/storage-secured-iframe.html @@ -0,0 +1,94 @@ + + + + + + + + + + diff --git a/devtools/server/tests/browser/storage-unsecured-iframe.html b/devtools/server/tests/browser/storage-unsecured-iframe.html new file mode 100644 index 000000000..d339fb7f2 --- /dev/null +++ b/devtools/server/tests/browser/storage-unsecured-iframe.html @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/devtools/server/tests/browser/storage-updates.html b/devtools/server/tests/browser/storage-updates.html new file mode 100644 index 000000000..6ffaf4316 --- /dev/null +++ b/devtools/server/tests/browser/storage-updates.html @@ -0,0 +1,47 @@ + + + + + + Storage inspector blank html for tests + + + + + diff --git a/devtools/server/tests/browser/stylesheets-nested-iframes.html b/devtools/server/tests/browser/stylesheets-nested-iframes.html new file mode 100644 index 000000000..7ee775323 --- /dev/null +++ b/devtools/server/tests/browser/stylesheets-nested-iframes.html @@ -0,0 +1,25 @@ + + + + + StyleSheetsActor iframe test + + + +

A test page with nested iframes

+ + + + diff --git a/devtools/server/tests/browser/timeline-iframe-child.html b/devtools/server/tests/browser/timeline-iframe-child.html new file mode 100644 index 000000000..5385c6485 --- /dev/null +++ b/devtools/server/tests/browser/timeline-iframe-child.html @@ -0,0 +1,19 @@ + + + + + Timeline iframe test - child frame + + +

Child frame

+ + + diff --git a/devtools/server/tests/browser/timeline-iframe-parent.html b/devtools/server/tests/browser/timeline-iframe-parent.html new file mode 100644 index 000000000..b94ba4259 --- /dev/null +++ b/devtools/server/tests/browser/timeline-iframe-parent.html @@ -0,0 +1,11 @@ + + + + + Timeline iframe test - parent frame + + +

Parent frame

+ + + diff --git a/devtools/server/tests/mochitest/.eslintrc.js b/devtools/server/tests/mochitest/.eslintrc.js new file mode 100644 index 000000000..c5b919ce3 --- /dev/null +++ b/devtools/server/tests/mochitest/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools mochitest eslintrc config. + "extends": "../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/server/tests/mochitest/Debugger.Source.prototype.element-2.js b/devtools/server/tests/mochitest/Debugger.Source.prototype.element-2.js new file mode 100644 index 000000000..eab746921 --- /dev/null +++ b/devtools/server/tests/mochitest/Debugger.Source.prototype.element-2.js @@ -0,0 +1 @@ +debugger; diff --git a/devtools/server/tests/mochitest/Debugger.Source.prototype.element.html b/devtools/server/tests/mochitest/Debugger.Source.prototype.element.html new file mode 100644 index 000000000..fcf4c6c85 --- /dev/null +++ b/devtools/server/tests/mochitest/Debugger.Source.prototype.element.html @@ -0,0 +1,17 @@ + + + + + + + + + + + +
Heidi
+ diff --git a/devtools/server/tests/mochitest/Debugger.Source.prototype.element.js b/devtools/server/tests/mochitest/Debugger.Source.prototype.element.js new file mode 100644 index 000000000..44115b373 --- /dev/null +++ b/devtools/server/tests/mochitest/Debugger.Source.prototype.element.js @@ -0,0 +1 @@ +function heinrichFun() { franz(); } diff --git a/devtools/server/tests/mochitest/animation-data.html b/devtools/server/tests/mochitest/animation-data.html new file mode 100644 index 000000000..01be59548 --- /dev/null +++ b/devtools/server/tests/mochitest/animation-data.html @@ -0,0 +1,120 @@ + + + + Animation Test Data + + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/chrome.ini b/devtools/server/tests/mochitest/chrome.ini new file mode 100644 index 000000000..ae69d163e --- /dev/null +++ b/devtools/server/tests/mochitest/chrome.ini @@ -0,0 +1,103 @@ +[DEFAULT] +tags = devtools +skip-if = os == 'android' +support-files = + animation-data.html + Debugger.Source.prototype.element.js + Debugger.Source.prototype.element-2.js + Debugger.Source.prototype.element.html + director-helpers.js + hello-actor.js + inspector_css-properties.html + inspector_getImageData.html + inspector-delay-image-response.sjs + inspector-eyedropper.html + inspector-helpers.js + inspector-search-data.html + inspector-styles-data.css + inspector-styles-data.html + inspector-traversal-data.html + large-image.jpg + memory-helpers.js + nonchrome_unsafeDereference.html + small-image.gif + setup-in-child.js + setup-in-parent.js + +[test_animation_actor-lifetime.html] +[test_connection-manager.html] +[test_connectToChild.html] +[test_css-logic.html] +[test_css-logic-media-queries.html] +[test_css-logic-specificity.html] +[test_css-properties_01.html] +[test_css-properties_02.html] +[test_Debugger.Source.prototype.introductionScript.html] +[test_Debugger.Source.prototype.introductionType.html] +[test_Debugger.Source.prototype.element.html] +[test_Debugger.Script.prototype.global.html] +[test_device.html] +[test_director.html] +[test_director_connectToChild.html] +[test_executeInGlobal-outerized_this.html] +[test_framerate_01.html] +[test_framerate_02.html] +[test_framerate_03.html] +[test_framerate_04.html] +[test_framerate_05.html] +[test_framerate_06.html] +[test_getProcess.html] +[test_inspector-anonymous.html] +[test_inspector-changeattrs.html] +[test_inspector-changevalue.html] +[test_inspector-dead-nodes.html] +[test_inspector-duplicate-node.html] +[test_inspector_getImageData.html] +[test_inspector_getImageDataFromURL.html] +[test_inspector_getImageData-wait-for-load.html] +[test_inspector_getNodeFromActor.html] +[test_inspector-hide.html] +[test_inspector-insert.html] +[test_inspector-mutations-attr.html] +[test_inspector-mutations-events.html] +[test_inspector-mutations-childlist.html] +[test_inspector-mutations-frameload.html] +[test_inspector-mutations-value.html] +[test_inspector-pick-color.html] +[test_inspector-pseudoclass-lock.html] +[test_inspector-release.html] +[test_inspector-reload.html] +[test_inspector-remove.html] +[test_inspector-resize.html] +[test_inspector-resolve-url.html] +[test_inspector-retain.html] +[test_inspector-search.html] +[test_inspector-search-front.html] +[test_inspector-scroll-into-view.html] +[test_inspector-traversal.html] +[test_makeGlobalObjectReference.html] +[test_memory.html] +[test_memory_allocations_01.html] +[test_memory_allocations_02.html] +[test_memory_allocations_03.html] +[test_memory_allocations_04.html] +[test_memory_allocations_05.html] +[test_memory_allocations_06.html] +[test_memory_allocations_07.html] +[test_memory_attach_01.html] +[test_memory_attach_02.html] +[test_memory_census.html] +[test_memory_gc_01.html] +[test_memory_gc_events.html] +[test_preference.html] +[test_settings.html] +[test_setupInParentChild.html] +[test_styles-applied.html] +[test_styles-computed.html] +[test_styles-layout.html] +[test_styles-matched.html] +[test_styles-modify.html] +[test_styles-svg.html] +[test_unsafeDereference.html] +[test_websocket-server.html] +skip-if = false diff --git a/devtools/server/tests/mochitest/director-helpers.js b/devtools/server/tests/mochitest/director-helpers.js new file mode 100644 index 000000000..fe1f7d394 --- /dev/null +++ b/devtools/server/tests/mochitest/director-helpers.js @@ -0,0 +1,44 @@ +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {DebuggerClient} = require("devtools/shared/client/main"); +const {DebuggerServer} = require("devtools/server/main"); +const Services = require("Services"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true); + +SimpleTest.registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled"); +}); + +const { DirectorRegistry } = require("devtools/server/actors/director-registry"); +const { DirectorRegistryFront } = require("devtools/shared/fronts/director-registry"); + +const { DirectorManagerFront } = require("devtools/shared/fronts/director-manager"); + +const { Task } = require("devtools/shared/task"); + +/** ********************************* + * director helpers functions + **********************************/ + +function* newConnectedDebuggerClient(opts) { + var transport = DebuggerServer.connectPipe(); + var client = new DebuggerClient(transport); + + yield client.connect(); + + var root = yield client.listTabs(); + + return { + client: client, + root: root, + transport: transport + }; +} + +function purgeInstalledDirectorScripts() { + DirectorRegistry.clear(); +} diff --git a/devtools/server/tests/mochitest/hello-actor.js b/devtools/server/tests/mochitest/hello-actor.js new file mode 100644 index 000000000..603192938 --- /dev/null +++ b/devtools/server/tests/mochitest/hello-actor.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const protocol = require("devtools/shared/protocol"); + +const helloSpec = protocol.generateActorSpec({ + typeName: "helloActor", + + methods: { + count: { + request: {}, + response: {count: protocol.RetVal("number")} + } + } +}); + +var HelloActor = protocol.ActorClassWithSpec(helloSpec, { + initialize: function () { + protocol.Actor.prototype.initialize.apply(this, arguments); + this.counter = 0; + }, + + count: function () { + return ++this.counter; + } +}); diff --git a/devtools/server/tests/mochitest/inspector-delay-image-response.sjs b/devtools/server/tests/mochitest/inspector-delay-image-response.sjs new file mode 100644 index 000000000..215d1e4d8 --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-delay-image-response.sjs @@ -0,0 +1,42 @@ +/** + * Adapted from https://dxr.mozilla.org/mozilla-central/rev/ + * 4e883591bb5dff021c108d3e30198a99547eed1e/layout/reftests/backgrounds/ + * delay-image-response.sjs + */ +"use strict"; + +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); + +// To avoid GC. +let timer = null; + +function handleRequest(request, response) { + let query = {}; + request.queryString.split("&").forEach(function(val) { + let [name, value] = val.split("="); + query[name] = unescape(value); + }); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + + // If there is no delay, we write the image and leave. + if (!("delay" in query)) { + response.write(IMAGE); + return; + } + + // If there is a delay, we create a timer which, when it fires, will write + // image and leave. + response.processAsync(); + const nsITimer = Components.interfaces.nsITimer; + + timer = Components.classes["@mozilla.org/timer;1"].createInstance(nsITimer); + timer.initWithCallback(function() { + response.write(IMAGE); + response.finish(); + }, query.delay, nsITimer.TYPE_ONE_SHOT); +} diff --git a/devtools/server/tests/mochitest/inspector-eyedropper.html b/devtools/server/tests/mochitest/inspector-eyedropper.html new file mode 100644 index 000000000..ded9f392d --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-eyedropper.html @@ -0,0 +1,18 @@ + + + + Inspector Eyedropper tests + + + + + + \ No newline at end of file diff --git a/devtools/server/tests/mochitest/inspector-helpers.js b/devtools/server/tests/mochitest/inspector-helpers.js new file mode 100644 index 000000000..47c643868 --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-helpers.js @@ -0,0 +1,310 @@ +var Cu = Components.utils; + +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {DebuggerClient} = require("devtools/shared/client/main"); +const {DebuggerServer} = require("devtools/server/main"); +const { Task } = require("devtools/shared/task"); + +const Services = require("Services"); +const promise = require("promise"); +const {_documentWalker} = require("devtools/server/actors/inspector"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +SimpleTest.registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.debugger.log"); +}); + + +if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + SimpleTest.registerCleanupFunction(function () { + DebuggerServer.destroy(); + }); +} + +var gAttachCleanups = []; + +SimpleTest.registerCleanupFunction(function () { + for (let cleanup of gAttachCleanups) { + cleanup(); + } +}); + +/** + * Open a tab, load the url, wait for it to signal its readiness, + * find the tab with the debugger server, and call the callback. + * + * Returns a function which can be called to close the opened ta + * and disconnect its debugger client. + */ +function attachURL(url, callback) { + var win = window.open(url, "_blank"); + var client = null; + + let cleanup = () => { + if (client) { + client.close(); + client = null; + } + if (win) { + win.close(); + win = null; + } + }; + gAttachCleanups.push(cleanup); + + window.addEventListener("message", function loadListener(event) { + if (event.data === "ready") { + client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect().then(([applicationType, traits]) => { + client.listTabs(response => { + for (let tab of response.tabs) { + if (tab.url === url) { + window.removeEventListener("message", loadListener, false); + client.attachTab(tab.actor, function (aResponse, aTabClient) { + try { + callback(null, client, tab, win.document); + } catch (ex) { + Cu.reportError(ex); + dump(ex); + } + }); + break; + } + } + }); + }); + } + }, false); + + return cleanup; +} + +function promiseOnce(target, event) { + let deferred = promise.defer(); + target.on(event, (...args) => { + if (args.length === 1) { + deferred.resolve(args[0]); + } else { + deferred.resolve(args); + } + }); + return deferred.promise; +} + +function sortOwnershipChildren(children) { + return children.sort((a, b) => a.name.localeCompare(b.name)); +} + +function serverOwnershipSubtree(walker, node) { + let actor = walker._refMap.get(node); + if (!actor) { + return undefined; + } + + let children = []; + let docwalker = new _documentWalker(node, window); + let child = docwalker.firstChild(); + while (child) { + let item = serverOwnershipSubtree(walker, child); + if (item) { + children.push(item); + } + child = docwalker.nextSibling(); + } + return { + name: actor.actorID, + children: sortOwnershipChildren(children) + }; +} + +function serverOwnershipTree(walker) { + let serverWalker = DebuggerServer._searchAllConnectionsForActor(walker.actorID); + + return { + root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc), + orphaned: [...serverWalker._orphaned].map(o => serverOwnershipSubtree(serverWalker, o.rawNode)), + retained: [...serverWalker._retainedOrphans].map(o => serverOwnershipSubtree(serverWalker, o.rawNode)) + }; +} + +function clientOwnershipSubtree(node) { + return { + name: node.actorID, + children: sortOwnershipChildren(node.treeChildren().map(child => clientOwnershipSubtree(child))) + }; +} + +function clientOwnershipTree(walker) { + return { + root: clientOwnershipSubtree(walker.rootNode), + orphaned: [...walker._orphaned].map(o => clientOwnershipSubtree(o)), + retained: [...walker._retainedOrphans].map(o => clientOwnershipSubtree(o)) + }; +} + +function ownershipTreeSize(tree) { + let size = 1; + for (let child of tree.children) { + size += ownershipTreeSize(child); + } + return size; +} + +function assertOwnershipTrees(walker) { + let serverTree = serverOwnershipTree(walker); + let clientTree = clientOwnershipTree(walker); + is(JSON.stringify(clientTree, null, " "), JSON.stringify(serverTree, null, " "), "Server and client ownership trees should match."); + + return ownershipTreeSize(clientTree.root); +} + +// Verify that an actorID is inaccessible both from the client library and the server. +function checkMissing(client, actorID) { + let deferred = promise.defer(); + let front = client.getActor(actorID); + ok(!front, "Front shouldn't be accessible from the client for actorID: " + actorID); + + deferred = promise.defer(); + client.request({ + to: actorID, + type: "request", + }, response => { + is(response.error, "noSuchActor", "node list actor should no longer be contactable."); + deferred.resolve(undefined); + }); + return deferred.promise; +} + +// Verify that an actorID is accessible both from the client library and the server. +function checkAvailable(client, actorID) { + let deferred = promise.defer(); + let front = client.getActor(actorID); + ok(front, "Front should be accessible from the client for actorID: " + actorID); + + deferred = promise.defer(); + client.request({ + to: actorID, + type: "garbageAvailableTest", + }, response => { + is(response.error, "unrecognizedPacketType", "node list actor should be contactable."); + deferred.resolve(undefined); + }); + return deferred.promise; +} + +function promiseDone(promise) { + promise.then(null, err => { + ok(false, "Promise failed: " + err); + if (err.stack) { + dump(err.stack); + } + SimpleTest.finish(); + }); +} + +// Mutation list testing + +function isSrcChange(change) { + return (change.type === "attributes" && change.attributeName === "src"); +} + +function assertAndStrip(mutations, message, test) { + let size = mutations.length; + mutations = mutations.filter(test); + ok((mutations.size != size), message); + return mutations; +} + +function isSrcChange(change) { + return change.type === "attributes" && change.attributeName === "src"; +} + +function isUnload(change) { + return change.type === "documentUnload"; +} + +function isFrameLoad(change) { + return change.type === "frameLoad"; +} + +function isUnretained(change) { + return change.type === "unretained"; +} + +function isChildList(change) { + return change.type === "childList"; +} + +function isNewRoot(change) { + return change.type === "newRoot"; +} + +// Make sure an iframe's src attribute changed and then +// strip that mutation out of the list. +function assertSrcChange(mutations) { + return assertAndStrip(mutations, "Should have had an iframe source change.", isSrcChange); +} + +// Make sure there's an unload in the mutation list and strip +// that mutation out of the list +function assertUnload(mutations) { + return assertAndStrip(mutations, "Should have had a document unload change.", isUnload); +} + +// Make sure there's a frame load in the mutation list and strip +// that mutation out of the list +function assertFrameLoad(mutations) { + return assertAndStrip(mutations, "Should have had a frame load change.", isFrameLoad); +} + +// Make sure there's a childList change in the mutation list and strip +// that mutation out of the list +function assertChildList(mutations) { + return assertAndStrip(mutations, "Should have had a frame load change.", isChildList); +} + +// Load mutations aren't predictable, so keep accumulating mutations until +// the one we're looking for shows up. +function waitForMutation(walker, test, mutations = []) { + let deferred = promise.defer(); + for (let change of mutations) { + if (test(change)) { + deferred.resolve(mutations); + } + } + + walker.once("mutations", newMutations => { + waitForMutation(walker, test, mutations.concat(newMutations)).then(finalMutations => { + deferred.resolve(finalMutations); + }); + }); + + return deferred.promise; +} + + +var _tests = []; +function addTest(test) { + _tests.push(test); +} + +function addAsyncTest(generator) { + _tests.push(() => Task.spawn(generator).then(null, ok.bind(null, false))); +} + +function runNextTest() { + if (_tests.length == 0) { + SimpleTest.finish(); + return; + } + var fn = _tests.shift(); + try { + fn(); + } catch (ex) { + info("Test function " + (fn.name ? "'" + fn.name + "' " : "") + + "threw an exception: " + ex); + } +} diff --git a/devtools/server/tests/mochitest/inspector-search-data.html b/devtools/server/tests/mochitest/inspector-search-data.html new file mode 100644 index 000000000..d0d5f9c68 --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-search-data.html @@ -0,0 +1,52 @@ + + + + Inspector Search Test Data + + + + + + + + +

Heading 1

+

A p tag with the text 'h1' inside of it. + A strong h1 result +

+ +
+ Unicode arrows +
+ +

Heading 2

+

Heading 2

+

Heading 2

+ +

Heading 3

+

Heading 3

+

Heading 3

+ +

Heading 4

+

Heading 4

+

Heading 4

+ +
+ + \ No newline at end of file diff --git a/devtools/server/tests/mochitest/inspector-styles-data.css b/devtools/server/tests/mochitest/inspector-styles-data.css new file mode 100644 index 000000000..5c3652f52 --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-styles-data.css @@ -0,0 +1,3 @@ +.external-rule { + cursor: crosshair; +} diff --git a/devtools/server/tests/mochitest/inspector-styles-data.html b/devtools/server/tests/mochitest/inspector-styles-data.html new file mode 100644 index 000000000..a2a126f0e --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-styles-data.html @@ -0,0 +1,81 @@ + + + + + +

Style Actor Tests

+ +
+
+
+
+
+ Here is the test node. +
+
+
+
+
+ + +
+
+ Here is the test node. +
+
+ + +
+
+ Here is the test node. +
+
+ +
+ Screen mediaqueried. +
+ +
+ +
+ +
I can has layout
+
I can has layout too
+ + + diff --git a/devtools/server/tests/mochitest/inspector-traversal-data.html b/devtools/server/tests/mochitest/inspector-traversal-data.html new file mode 100644 index 000000000..45b8c2ede --- /dev/null +++ b/devtools/server/tests/mochitest/inspector-traversal-data.html @@ -0,0 +1,90 @@ + + + + Inspector Traversal Test Data + + + + +

Inspector Actor Tests

+ longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong + short + +
+
a
+
b
+
c
+
d
+
e
+
f
+
g
+
h
+
i
+
j
+
k
+
l
+
m
+
n
+
o
+
p
+
q
+
r
+
s
+
t
+
u
+
v
+
w
+
x
+
y
+
z
+
+
+
+
+

+ + +
middle
+
+
light dom
+ +
+
+
+
scroll
+ + diff --git a/devtools/server/tests/mochitest/inspector_css-properties.html b/devtools/server/tests/mochitest/inspector_css-properties.html new file mode 100644 index 000000000..2c160c928 --- /dev/null +++ b/devtools/server/tests/mochitest/inspector_css-properties.html @@ -0,0 +1,10 @@ + + + + + + diff --git a/devtools/server/tests/mochitest/inspector_getImageData.html b/devtools/server/tests/mochitest/inspector_getImageData.html new file mode 100644 index 000000000..eebecea90 --- /dev/null +++ b/devtools/server/tests/mochitest/inspector_getImageData.html @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/devtools/server/tests/mochitest/large-image.jpg b/devtools/server/tests/mochitest/large-image.jpg new file mode 100644 index 000000000..bda383e59 Binary files /dev/null and b/devtools/server/tests/mochitest/large-image.jpg differ diff --git a/devtools/server/tests/mochitest/memory-helpers.js b/devtools/server/tests/mochitest/memory-helpers.js new file mode 100644 index 000000000..aea8c4732 --- /dev/null +++ b/devtools/server/tests/mochitest/memory-helpers.js @@ -0,0 +1,52 @@ +var Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; + +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { Task } = require("devtools/shared/task"); +var Services = require("Services"); +var { DebuggerClient } = require("devtools/shared/client/main"); +var { DebuggerServer } = require("devtools/server/main"); + +var { MemoryFront } = require("devtools/shared/fronts/memory"); + +// Always log packets when running tests. +Services.prefs.setBoolPref("devtools.debugger.log", true); +SimpleTest.registerCleanupFunction(function () { + Services.prefs.clearUserPref("devtools.debugger.log"); +}); + +function startServerAndGetSelectedTabMemory() { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + var client = new DebuggerClient(DebuggerServer.connectPipe()); + + return client.connect() + .then(() => client.listTabs()) + .then(response => { + var form = response.tabs[response.selected]; + var memory = MemoryFront(client, form, response); + + return { memory, client }; + }); +} + +function destroyServerAndFinish(client) { + client.close().then(() => { + DebuggerServer.destroy(); + SimpleTest.finish(); + }); +} + +function waitForTime(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} + +function waitUntil(predicate) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => setTimeout(() => waitUntil(predicate).then(() => resolve(true)), 10)); +} diff --git a/devtools/server/tests/mochitest/nonchrome_unsafeDereference.html b/devtools/server/tests/mochitest/nonchrome_unsafeDereference.html new file mode 100644 index 000000000..6c19d3104 --- /dev/null +++ b/devtools/server/tests/mochitest/nonchrome_unsafeDereference.html @@ -0,0 +1,8 @@ + + + + diff --git a/devtools/server/tests/mochitest/setup-in-child.js b/devtools/server/tests/mochitest/setup-in-child.js new file mode 100644 index 000000000..a575faa20 --- /dev/null +++ b/devtools/server/tests/mochitest/setup-in-child.js @@ -0,0 +1,20 @@ +const {Cc, Ci} = require("chrome"); +const cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]. + getService(Ci.nsIMessageListenerManager); +const { DebuggerServer } = require("devtools/server/main"); + +exports.setupChild = function (a, b, c) { + cpmm.sendAsyncMessage("test:setupChild", [a, b, c]); +}; + +exports.callParent = function () { + // Hack! Fetch DebuggerServerConnection objects directly within DebuggerServer guts. + for (let id in DebuggerServer._connections) { + let conn = DebuggerServer._connections[id]; + conn.setupInParent({ + module: "chrome://mochitests/content/chrome/devtools/server/tests/mochitest/setup-in-parent.js", + setupParent: "setupParent", + args: [{one: true}, 2, "three"] + }); + } +}; diff --git a/devtools/server/tests/mochitest/setup-in-parent.js b/devtools/server/tests/mochitest/setup-in-parent.js new file mode 100644 index 000000000..9845ee647 --- /dev/null +++ b/devtools/server/tests/mochitest/setup-in-parent.js @@ -0,0 +1,10 @@ +var {Ci} = require("chrome"); +var Services = require("Services"); + +exports.setupParent = function ({mm, prefix}) { + let args = [ + !!mm.QueryInterface(Ci.nsIMessageSender), + prefix + ]; + Services.obs.notifyObservers(null, "test:setupParent", JSON.stringify(args)); +}; diff --git a/devtools/server/tests/mochitest/small-image.gif b/devtools/server/tests/mochitest/small-image.gif new file mode 100644 index 000000000..e702427a5 Binary files /dev/null and b/devtools/server/tests/mochitest/small-image.gif differ diff --git a/devtools/server/tests/mochitest/test_Debugger.Script.prototype.global.html b/devtools/server/tests/mochitest/test_Debugger.Script.prototype.global.html new file mode 100644 index 000000000..77357e608 --- /dev/null +++ b/devtools/server/tests/mochitest/test_Debugger.Script.prototype.global.html @@ -0,0 +1,48 @@ + + + + + + Debugger.Script.prototype.global should return inner windows + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_Debugger.Source.prototype.element.html b/devtools/server/tests/mochitest/test_Debugger.Source.prototype.element.html new file mode 100644 index 000000000..3cbc22353 --- /dev/null +++ b/devtools/server/tests/mochitest/test_Debugger.Source.prototype.element.html @@ -0,0 +1,182 @@ + + + + + + Debugger.Source.prototype.element should return owning element + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionScript.html b/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionScript.html new file mode 100644 index 000000000..cbc2ae615 --- /dev/null +++ b/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionScript.html @@ -0,0 +1,97 @@ + + + + + + Debugger.Source.prototype.introductionScript with no caller + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionType.html b/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionType.html new file mode 100644 index 000000000..c0066659c --- /dev/null +++ b/devtools/server/tests/mochitest/test_Debugger.Source.prototype.introductionType.html @@ -0,0 +1,181 @@ + + + + + + Debugger.Source.prototype.introductionType should identify event handlers + + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_animation_actor-lifetime.html b/devtools/server/tests/mochitest/test_animation_actor-lifetime.html new file mode 100644 index 000000000..a5265d918 --- /dev/null +++ b/devtools/server/tests/mochitest/test_animation_actor-lifetime.html @@ -0,0 +1,91 @@ + + + + + + Test for Bug 1247243 + + + + + + + + Mozilla Bug 1247243 + Test Document + + diff --git a/devtools/server/tests/mochitest/test_connectToChild.html b/devtools/server/tests/mochitest/test_connectToChild.html new file mode 100644 index 000000000..3bda7b566 --- /dev/null +++ b/devtools/server/tests/mochitest/test_connectToChild.html @@ -0,0 +1,134 @@ + + + + + + Mozilla Bug + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_connection-manager.html b/devtools/server/tests/mochitest/test_connection-manager.html new file mode 100644 index 000000000..bc802f933 --- /dev/null +++ b/devtools/server/tests/mochitest/test_connection-manager.html @@ -0,0 +1,119 @@ + + + + + + Mozilla Bug + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_css-logic-media-queries.html b/devtools/server/tests/mochitest/test_css-logic-media-queries.html new file mode 100644 index 000000000..bc465df55 --- /dev/null +++ b/devtools/server/tests/mochitest/test_css-logic-media-queries.html @@ -0,0 +1,62 @@ + + + + + + Test css-logic media-queries + + + + + +
+ + + diff --git a/devtools/server/tests/mochitest/test_css-logic-specificity.html b/devtools/server/tests/mochitest/test_css-logic-specificity.html new file mode 100644 index 000000000..45169c1fd --- /dev/null +++ b/devtools/server/tests/mochitest/test_css-logic-specificity.html @@ -0,0 +1,84 @@ + + + + + + Test css-logic specificity + + + + + + diff --git a/devtools/server/tests/mochitest/test_css-logic.html b/devtools/server/tests/mochitest/test_css-logic.html new file mode 100644 index 000000000..6c21e72c8 --- /dev/null +++ b/devtools/server/tests/mochitest/test_css-logic.html @@ -0,0 +1,167 @@ + + + + + + Test for Bug + + + + + + + +
+
+
+
+
+
+

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + diff --git a/devtools/server/tests/mochitest/test_css-properties_01.html b/devtools/server/tests/mochitest/test_css-properties_01.html new file mode 100644 index 000000000..45386b830 --- /dev/null +++ b/devtools/server/tests/mochitest/test_css-properties_01.html @@ -0,0 +1,121 @@ + + + + + + Test CSS Properties Actor + + + + + + + Mozilla Bug 1265798 + Test Document + + diff --git a/devtools/server/tests/mochitest/test_css-properties_02.html b/devtools/server/tests/mochitest/test_css-properties_02.html new file mode 100644 index 000000000..1a5d99d72 --- /dev/null +++ b/devtools/server/tests/mochitest/test_css-properties_02.html @@ -0,0 +1,86 @@ + + + + + + Test CSS Properties Actor + + + + + + + Mozilla Bug 1265798 + Test Document + + diff --git a/devtools/server/tests/mochitest/test_device.html b/devtools/server/tests/mochitest/test_device.html new file mode 100644 index 000000000..d678f185f --- /dev/null +++ b/devtools/server/tests/mochitest/test_device.html @@ -0,0 +1,99 @@ + + + + + + Mozilla Bug + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_director.html b/devtools/server/tests/mochitest/test_director.html new file mode 100644 index 000000000..ad2648bfa --- /dev/null +++ b/devtools/server/tests/mochitest/test_director.html @@ -0,0 +1,114 @@ + + + + + + Test for Bug + + + + + +
+  
+  
+
+ + diff --git a/devtools/server/tests/mochitest/test_director_connectToChild.html b/devtools/server/tests/mochitest/test_director_connectToChild.html new file mode 100644 index 000000000..cb348efe6 --- /dev/null +++ b/devtools/server/tests/mochitest/test_director_connectToChild.html @@ -0,0 +1,98 @@ + + + + + + Test for Bug + + + + + +
+  
+  
+
+ + diff --git a/devtools/server/tests/mochitest/test_executeInGlobal-outerized_this.html b/devtools/server/tests/mochitest/test_executeInGlobal-outerized_this.html new file mode 100644 index 000000000..8bedde618 --- /dev/null +++ b/devtools/server/tests/mochitest/test_executeInGlobal-outerized_this.html @@ -0,0 +1,69 @@ + + + + + + Mozilla Bug 837060 + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_framerate_01.html b/devtools/server/tests/mochitest/test_framerate_01.html new file mode 100644 index 000000000..0282d50a2 --- /dev/null +++ b/devtools/server/tests/mochitest/test_framerate_01.html @@ -0,0 +1,141 @@ + + + + + + Framerate actor test + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_framerate_02.html b/devtools/server/tests/mochitest/test_framerate_02.html new file mode 100644 index 000000000..9d4626b12 --- /dev/null +++ b/devtools/server/tests/mochitest/test_framerate_02.html @@ -0,0 +1,113 @@ + + + + + + Framerate actor test + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_framerate_03.html b/devtools/server/tests/mochitest/test_framerate_03.html new file mode 100644 index 000000000..da76ebc20 --- /dev/null +++ b/devtools/server/tests/mochitest/test_framerate_03.html @@ -0,0 +1,82 @@ + + + + + + Framerate actor test + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_framerate_04.html b/devtools/server/tests/mochitest/test_framerate_04.html new file mode 100644 index 000000000..af6747291 --- /dev/null +++ b/devtools/server/tests/mochitest/test_framerate_04.html @@ -0,0 +1,72 @@ + + + + + + Framerate actor test + + + + + +
+
+
+Test Document + + diff --git a/devtools/server/tests/mochitest/test_framerate_05.html b/devtools/server/tests/mochitest/test_framerate_05.html new file mode 100644 index 000000000..96f56a18f --- /dev/null +++ b/devtools/server/tests/mochitest/test_framerate_05.html @@ -0,0 +1,77 @@ + + + + + + Framerate actor test + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_framerate_06.html b/devtools/server/tests/mochitest/test_framerate_06.html new file mode 100644 index 000000000..ecb0a71e0 --- /dev/null +++ b/devtools/server/tests/mochitest/test_framerate_06.html @@ -0,0 +1,82 @@ + + + + + + Framerate actor test + + + + + +
+
+
+Test Document + + diff --git a/devtools/server/tests/mochitest/test_getProcess.html b/devtools/server/tests/mochitest/test_getProcess.html new file mode 100644 index 000000000..3c8ca5727 --- /dev/null +++ b/devtools/server/tests/mochitest/test_getProcess.html @@ -0,0 +1,120 @@ + + + + + + Mozilla Bug + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-anonymous.html b/devtools/server/tests/mochitest/test_inspector-anonymous.html new file mode 100644 index 000000000..56a911c89 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-anonymous.html @@ -0,0 +1,201 @@ + + + + + + Test for Bug 777674 + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-changeattrs.html b/devtools/server/tests/mochitest/test_inspector-changeattrs.html new file mode 100644 index 000000000..23b7660d2 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-changeattrs.html @@ -0,0 +1,99 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-changevalue.html b/devtools/server/tests/mochitest/test_inspector-changevalue.html new file mode 100644 index 000000000..a5b613157 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-changevalue.html @@ -0,0 +1,82 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-dead-nodes.html b/devtools/server/tests/mochitest/test_inspector-dead-nodes.html new file mode 100644 index 000000000..274636cd6 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-dead-nodes.html @@ -0,0 +1,386 @@ + + + + + + Test for Bug 1121528 + + + + + + + +Mozilla Bug 1121528 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-duplicate-node.html b/devtools/server/tests/mochitest/test_inspector-duplicate-node.html new file mode 100644 index 000000000..35722f226 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-duplicate-node.html @@ -0,0 +1,75 @@ + + + + + + Test for Bug 1208864 + + + + + + + +Mozilla Bug 1208864 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-hide.html b/devtools/server/tests/mochitest/test_inspector-hide.html new file mode 100644 index 000000000..d9b134c22 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-hide.html @@ -0,0 +1,76 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-insert.html b/devtools/server/tests/mochitest/test_inspector-insert.html new file mode 100644 index 000000000..82b4fef3e --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-insert.html @@ -0,0 +1,119 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-mutations-attr.html b/devtools/server/tests/mochitest/test_inspector-mutations-attr.html new file mode 100644 index 000000000..15c14608b --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-mutations-attr.html @@ -0,0 +1,167 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-mutations-childlist.html b/devtools/server/tests/mochitest/test_inspector-mutations-childlist.html new file mode 100644 index 000000000..d845b987e --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-mutations-childlist.html @@ -0,0 +1,310 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-mutations-events.html b/devtools/server/tests/mochitest/test_inspector-mutations-events.html new file mode 100644 index 000000000..992bc7f8d --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-mutations-events.html @@ -0,0 +1,183 @@ + + + + + + Test for Bug 1157469 + + + + + + +Mozilla Bug 1157469 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-mutations-frameload.html b/devtools/server/tests/mochitest/test_inspector-mutations-frameload.html new file mode 100644 index 000000000..54966cea7 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-mutations-frameload.html @@ -0,0 +1,214 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-mutations-value.html b/devtools/server/tests/mochitest/test_inspector-mutations-value.html new file mode 100644 index 000000000..352526b13 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-mutations-value.html @@ -0,0 +1,168 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-pick-color.html b/devtools/server/tests/mochitest/test_inspector-pick-color.html new file mode 100644 index 000000000..48ad08468 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-pick-color.html @@ -0,0 +1,101 @@ + + + + + + Test for Bug 1262439 + + + + + + +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html b/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html new file mode 100644 index 000000000..64bb03f80 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-pseudoclass-lock.html @@ -0,0 +1,174 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-release.html b/devtools/server/tests/mochitest/test_inspector-release.html new file mode 100644 index 000000000..45412bef0 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-release.html @@ -0,0 +1,102 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-reload.html b/devtools/server/tests/mochitest/test_inspector-reload.html new file mode 100644 index 000000000..91252aa8f --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-reload.html @@ -0,0 +1,80 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-remove.html b/devtools/server/tests/mochitest/test_inspector-remove.html new file mode 100644 index 000000000..2331c3e30 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-remove.html @@ -0,0 +1,117 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-resize.html b/devtools/server/tests/mochitest/test_inspector-resize.html new file mode 100644 index 000000000..eafa6436c --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-resize.html @@ -0,0 +1,77 @@ + + + + + + Test for Bug 1222409 + + + + + + +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-resolve-url.html b/devtools/server/tests/mochitest/test_inspector-resolve-url.html new file mode 100644 index 000000000..1494739ed --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-resolve-url.html @@ -0,0 +1,88 @@ + + + + + + Test for Bug 921102 + + + + + + + +Mozilla Bug 921102 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-retain.html b/devtools/server/tests/mochitest/test_inspector-retain.html new file mode 100644 index 000000000..e8342cf67 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-retain.html @@ -0,0 +1,180 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-scroll-into-view.html b/devtools/server/tests/mochitest/test_inspector-scroll-into-view.html new file mode 100644 index 000000000..1e164e83d --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-scroll-into-view.html @@ -0,0 +1,87 @@ + + + + + + Test for Bug 901250 + + + + + + + +Mozilla Bug 901250 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-search-front.html b/devtools/server/tests/mochitest/test_inspector-search-front.html new file mode 100644 index 000000000..e0f8f77e8 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-search-front.html @@ -0,0 +1,217 @@ + + + + + + Test for Bug 835896 + + + + + + +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-search.html b/devtools/server/tests/mochitest/test_inspector-search.html new file mode 100644 index 000000000..623d3018d --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-search.html @@ -0,0 +1,296 @@ + + + + + + Test for Bug 835896 + + + + + + +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector-traversal.html b/devtools/server/tests/mochitest/test_inspector-traversal.html new file mode 100644 index 000000000..ffac8e915 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector-traversal.html @@ -0,0 +1,354 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector_getImageData-wait-for-load.html b/devtools/server/tests/mochitest/test_inspector_getImageData-wait-for-load.html new file mode 100644 index 000000000..63eb0bd3c --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector_getImageData-wait-for-load.html @@ -0,0 +1,136 @@ + + + + + + Test for Bug 1192536 + + + + + + + +Mozilla Bug 1192536 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector_getImageData.html b/devtools/server/tests/mochitest/test_inspector_getImageData.html new file mode 100644 index 000000000..be3c24194 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector_getImageData.html @@ -0,0 +1,166 @@ + + + + + + Test for Bug 932937 + + + + + + + +Mozilla Bug 932937 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector_getImageDataFromURL.html b/devtools/server/tests/mochitest/test_inspector_getImageDataFromURL.html new file mode 100644 index 000000000..473a62275 --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector_getImageDataFromURL.html @@ -0,0 +1,114 @@ + + + + + + Test for Bug 1192536 + + + + + + + +Mozilla Bug 1192536 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html b/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html new file mode 100644 index 000000000..6c06d8a7b --- /dev/null +++ b/devtools/server/tests/mochitest/test_inspector_getNodeFromActor.html @@ -0,0 +1,88 @@ + + + + + + Test for Bug 1155653 + + + + + + + +Mozilla Bug 1155653 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_makeGlobalObjectReference.html b/devtools/server/tests/mochitest/test_makeGlobalObjectReference.html new file mode 100644 index 000000000..8bd7e0476 --- /dev/null +++ b/devtools/server/tests/mochitest/test_makeGlobalObjectReference.html @@ -0,0 +1,86 @@ + + + + + + Mozilla Bug 914405 + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory.html b/devtools/server/tests/mochitest/test_memory.html new file mode 100644 index 000000000..9f191da76 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory.html @@ -0,0 +1,37 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_01.html b/devtools/server/tests/mochitest/test_memory_allocations_01.html new file mode 100644 index 000000000..2ed9b74bc --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_01.html @@ -0,0 +1,98 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_02.html b/devtools/server/tests/mochitest/test_memory_allocations_02.html new file mode 100644 index 000000000..0133a27b0 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_02.html @@ -0,0 +1,76 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_03.html b/devtools/server/tests/mochitest/test_memory_allocations_03.html new file mode 100644 index 000000000..b7d18d7ed --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_03.html @@ -0,0 +1,78 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_04.html b/devtools/server/tests/mochitest/test_memory_allocations_04.html new file mode 100644 index 000000000..5568736d3 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_04.html @@ -0,0 +1,60 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_05.html b/devtools/server/tests/mochitest/test_memory_allocations_05.html new file mode 100644 index 000000000..0eeb7bd16 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_05.html @@ -0,0 +1,87 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_06.html b/devtools/server/tests/mochitest/test_memory_allocations_06.html new file mode 100644 index 000000000..56a9f8041 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_06.html @@ -0,0 +1,49 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_allocations_07.html b/devtools/server/tests/mochitest/test_memory_allocations_07.html new file mode 100644 index 000000000..c26c2d8ec --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_allocations_07.html @@ -0,0 +1,55 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_attach_01.html b/devtools/server/tests/mochitest/test_memory_attach_01.html new file mode 100644 index 000000000..5b0b3f75e --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_attach_01.html @@ -0,0 +1,31 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_attach_02.html b/devtools/server/tests/mochitest/test_memory_attach_02.html new file mode 100644 index 000000000..76269a6df --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_attach_02.html @@ -0,0 +1,49 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_census.html b/devtools/server/tests/mochitest/test_memory_census.html new file mode 100644 index 000000000..f24050337 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_census.html @@ -0,0 +1,33 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_gc_01.html b/devtools/server/tests/mochitest/test_memory_gc_01.html new file mode 100644 index 000000000..97cb754f0 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_gc_01.html @@ -0,0 +1,46 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_memory_gc_events.html b/devtools/server/tests/mochitest/test_memory_gc_events.html new file mode 100644 index 000000000..2297481d4 --- /dev/null +++ b/devtools/server/tests/mochitest/test_memory_gc_events.html @@ -0,0 +1,42 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_preference.html b/devtools/server/tests/mochitest/test_preference.html new file mode 100644 index 000000000..54903f455 --- /dev/null +++ b/devtools/server/tests/mochitest/test_preference.html @@ -0,0 +1,115 @@ + + + + + + Test Preference Actor + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_settings.html b/devtools/server/tests/mochitest/test_settings.html new file mode 100644 index 000000000..5665b46b3 --- /dev/null +++ b/devtools/server/tests/mochitest/test_settings.html @@ -0,0 +1,130 @@ + + + + + + Test Settings Actor + + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_setupInParentChild.html b/devtools/server/tests/mochitest/test_setupInParentChild.html new file mode 100644 index 000000000..fc94ca96a --- /dev/null +++ b/devtools/server/tests/mochitest/test_setupInParentChild.html @@ -0,0 +1,110 @@ + + + + + + Mozilla Bug + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_styles-applied.html b/devtools/server/tests/mochitest/test_styles-applied.html new file mode 100644 index 000000000..d9fb6ec7f --- /dev/null +++ b/devtools/server/tests/mochitest/test_styles-applied.html @@ -0,0 +1,145 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_styles-computed.html b/devtools/server/tests/mochitest/test_styles-computed.html new file mode 100644 index 000000000..c70adc8eb --- /dev/null +++ b/devtools/server/tests/mochitest/test_styles-computed.html @@ -0,0 +1,139 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_styles-layout.html b/devtools/server/tests/mochitest/test_styles-layout.html new file mode 100644 index 000000000..b2134b0c9 --- /dev/null +++ b/devtools/server/tests/mochitest/test_styles-layout.html @@ -0,0 +1,116 @@ + + + + +Test for Bug 1175040 - PageStyleActor.getLayout + + + + + + +Mozilla Bug 1175040 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_styles-matched.html b/devtools/server/tests/mochitest/test_styles-matched.html new file mode 100644 index 000000000..4d24fbe70 --- /dev/null +++ b/devtools/server/tests/mochitest/test_styles-matched.html @@ -0,0 +1,98 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_styles-modify.html b/devtools/server/tests/mochitest/test_styles-modify.html new file mode 100644 index 000000000..5a8e20bc3 --- /dev/null +++ b/devtools/server/tests/mochitest/test_styles-modify.html @@ -0,0 +1,116 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_styles-svg.html b/devtools/server/tests/mochitest/test_styles-svg.html new file mode 100644 index 000000000..51a84420c --- /dev/null +++ b/devtools/server/tests/mochitest/test_styles-svg.html @@ -0,0 +1,70 @@ + + + + + + Test for Bug + + + + + + + +Mozilla Bug 921191 +Test Document +

+ +
+
+ + diff --git a/devtools/server/tests/mochitest/test_unsafeDereference.html b/devtools/server/tests/mochitest/test_unsafeDereference.html new file mode 100644 index 000000000..df44fac51 --- /dev/null +++ b/devtools/server/tests/mochitest/test_unsafeDereference.html @@ -0,0 +1,52 @@ + + + + + + Mozilla Bug 837723 + + + + +
+
+
+ + diff --git a/devtools/server/tests/mochitest/test_websocket-server.html b/devtools/server/tests/mochitest/test_websocket-server.html new file mode 100644 index 000000000..583d96dd9 --- /dev/null +++ b/devtools/server/tests/mochitest/test_websocket-server.html @@ -0,0 +1,82 @@ + + + + + Mozilla Bug + + + + + + + + diff --git a/devtools/server/tests/unit/.eslintrc.js b/devtools/server/tests/unit/.eslintrc.js new file mode 100644 index 000000000..012428019 --- /dev/null +++ b/devtools/server/tests/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/server/tests/unit/addons/web-extension-upgrade/manifest.json b/devtools/server/tests/unit/addons/web-extension-upgrade/manifest.json new file mode 100644 index 000000000..f70b11efd --- /dev/null +++ b/devtools/server/tests/unit/addons/web-extension-upgrade/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor Upgrade", + "version": "1.0", + "applications": { + "gecko": { + "id": "test-addons-actor@mozilla.org" + } + } +} diff --git a/devtools/server/tests/unit/addons/web-extension/manifest.json b/devtools/server/tests/unit/addons/web-extension/manifest.json new file mode 100644 index 000000000..d120cf3da --- /dev/null +++ b/devtools/server/tests/unit/addons/web-extension/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor", + "version": "1.0", + "applications": { + "gecko": { + "id": "test-addons-actor@mozilla.org" + } + } +} diff --git a/devtools/server/tests/unit/addons/web-extension2/manifest.json b/devtools/server/tests/unit/addons/web-extension2/manifest.json new file mode 100644 index 000000000..57daae29d --- /dev/null +++ b/devtools/server/tests/unit/addons/web-extension2/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor 2", + "version": "1.0", + "applications": { + "gecko": { + "id": "test-addons-actor2@mozilla.org" + } + } +} diff --git a/devtools/server/tests/unit/babel_and_browserify_script_with_source_map.js b/devtools/server/tests/unit/babel_and_browserify_script_with_source_map.js new file mode 100644 index 000000000..317eb68ca --- /dev/null +++ b/devtools/server/tests/unit/babel_and_browserify_script_with_source_map.js @@ -0,0 +1,79 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { + DebuggerServer.registerModule("devtools/server/actors/heap-snapshot-file", { + prefix: "heapSnapshotFile", + constructor: "HeapSnapshotFileActor", + type: { global: true } + }); + + getTestTab(client, TEST_GLOBAL_NAME, function (tabForm, rootForm) { + if (!tabForm || !rootForm) { + ok(false, "Could not attach to test tab: " + TEST_GLOBAL_NAME); + return; + } + + Task.spawn(function* () { + try { + const memoryFront = new MemoryFront(client, tabForm, rootForm); + yield memoryFront.attach(); + yield* testGeneratorFunction(client, memoryFront); + yield memoryFront.detach(); + } catch (err) { + DevToolsUtils.reportException("makeMemoryActorTest", err); + ok(false, "Got an error: " + err); + } + + finishClient(client); + }); + }); + }); + }; +} + +/** + * Save as makeMemoryActorTest but attaches the MemoryFront to the MemoryActor + * scoped to the full runtime rather than to a tab. + */ +function makeFullRuntimeMemoryActorTest(testGeneratorFunction) { + return function run_test() { + do_test_pending(); + startTestDebuggerServer("test_MemoryActor").then(client => { + DebuggerServer.registerModule("devtools/server/actors/heap-snapshot-file", { + prefix: "heapSnapshotFile", + constructor: "HeapSnapshotFileActor", + type: { global: true } + }); + + getChromeActors(client).then(function (form) { + if (!form) { + ok(false, "Could not attach to chrome actors"); + return; + } + + Task.spawn(function* () { + try { + const rootForm = yield listTabs(client); + const memoryFront = new MemoryFront(client, form, rootForm); + yield memoryFront.attach(); + yield* testGeneratorFunction(client, memoryFront); + yield memoryFront.detach(); + } catch (err) { + DevToolsUtils.reportException("makeMemoryActorTest", err); + ok(false, "Got an error: " + err); + } + + finishClient(client); + }); + }); + }); + }; +} + +function createTestGlobal(name) { + let sandbox = Cu.Sandbox( + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + sandbox.__name = name; + return sandbox; +} + +function connect(client) { + dump("Connecting client.\n"); + return client.connect(); +} + +function close(client) { + dump("Closing client.\n"); + return client.close(); +} + +function listTabs(client) { + dump("Listing tabs.\n"); + return client.listTabs(); +} + +function findTab(tabs, title) { + dump("Finding tab with title '" + title + "'.\n"); + for (let tab of tabs) { + if (tab.title === title) { + return tab; + } + } + return null; +} + +function attachTab(client, tab) { + dump("Attaching to tab with title '" + tab.title + "'.\n"); + return client.attachTab(tab.actor); +} + +function waitForNewSource(threadClient, url) { + dump("Waiting for new source with url '" + url + "'.\n"); + return waitForEvent(threadClient, "newSource", function (packet) { + return packet.source.url === url; + }); +} + +function attachThread(tabClient, options = {}) { + dump("Attaching to thread.\n"); + return tabClient.attachThread(options); +} + +function resume(threadClient) { + dump("Resuming thread.\n"); + return threadClient.resume(); +} + +function getSources(threadClient) { + dump("Getting sources.\n"); + return threadClient.getSources(); +} + +function findSource(sources, url) { + dump("Finding source with url '" + url + "'.\n"); + for (let source of sources) { + if (source.url === url) { + return source; + } + } + return null; +} + +function waitForPause(threadClient) { + dump("Waiting for pause.\n"); + return waitForEvent(threadClient, "paused"); +} + +function setBreakpoint(sourceClient, location) { + dump("Setting breakpoint.\n"); + return sourceClient.setBreakpoint(location); +} + +function dumpn(msg) { + dump("DBG-TEST: " + msg + "\n"); +} + +function testExceptionHook(ex) { + try { + do_report_unexpected_exception(ex); + } catch (ex) { + return {throw: ex}; + } + return undefined; +} + +// Convert an nsIScriptError 'aFlags' value into an appropriate string. +function scriptErrorFlagsToKind(aFlags) { + var kind; + if (aFlags & Ci.nsIScriptError.warningFlag) + kind = "warning"; + if (aFlags & Ci.nsIScriptError.exceptionFlag) + kind = "exception"; + else + kind = "error"; + + if (aFlags & Ci.nsIScriptError.strictFlag) + kind = "strict " + kind; + + return kind; +} + +// Register a console listener, so console messages don't just disappear +// into the ether. +var errorCount = 0; +var listener = { + observe: function (aMessage) { + try { + errorCount++; + try { + // If we've been given an nsIScriptError, then we can print out + // something nicely formatted, for tools like Emacs to pick up. + var scriptError = aMessage.QueryInterface(Ci.nsIScriptError); + dumpn(aMessage.sourceName + ":" + aMessage.lineNumber + ": " + + scriptErrorFlagsToKind(aMessage.flags) + ": " + + aMessage.errorMessage); + var string = aMessage.errorMessage; + } catch (x) { + // Be a little paranoid with message, as the whole goal here is to lose + // no information. + try { + var string = "" + aMessage.message; + } catch (x) { + var string = ""; + } + } + + // Make sure we exit all nested event loops so that the test can finish. + while (DebuggerServer + && DebuggerServer.xpcInspector + && DebuggerServer.xpcInspector.eventLoopNestLevel > 0) { + DebuggerServer.xpcInspector.exitNestedEventLoop(); + } + + // In the world before bug 997440, exceptions were getting lost because of + // the arbitrary JSContext being used in nsXPCWrappedJSClass::CallMethod. + // In the new world, the wanderers have returned. However, because of the, + // currently very-broken, exception reporting machinery in + // XPCWrappedJSClass these get reported as errors to the console, even if + // there's actually JS on the stack above that will catch them. If we + // throw an error here because of them our tests start failing. So, we'll + // just dump the message to the logs instead, to make sure the information + // isn't lost. + dumpn("head_dbg.js observed a console message: " + string); + } catch (_) { + // Swallow everything to avoid console reentrancy errors. We did our best + // to log above, but apparently that didn't cut it. + } + } +}; + +var consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService); +consoleService.registerListener(listener); + +function check_except(func) +{ + try { + func(); + } catch (e) { + do_check_true(true); + return; + } + dumpn("Should have thrown an exception: " + func.toString()); + do_check_true(false); +} + +function testGlobal(aName) { + let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + + let sandbox = Cu.Sandbox(systemPrincipal); + sandbox.__name = aName; + return sandbox; +} + +function addTestGlobal(aName, aServer = DebuggerServer) +{ + let global = testGlobal(aName); + aServer.addTestGlobal(global); + return global; +} + +// List the DebuggerClient |aClient|'s tabs, look for one whose title is +// |aTitle|, and apply |aCallback| to the packet's entry for that tab. +function getTestTab(aClient, aTitle, aCallback) { + aClient.listTabs(function (aResponse) { + for (let tab of aResponse.tabs) { + if (tab.title === aTitle) { + aCallback(tab, aResponse); + return; + } + } + aCallback(null); + }); +} + +// Attach to |aClient|'s tab whose title is |aTitle|; pass |aCallback| the +// response packet and a TabClient instance referring to that tab. +function attachTestTab(aClient, aTitle, aCallback) { + getTestTab(aClient, aTitle, function (aTab) { + aClient.attachTab(aTab.actor, aCallback); + }); +} + +// Attach to |aClient|'s tab whose title is |aTitle|, and then attach to +// that tab's thread. Pass |aCallback| the thread attach response packet, a +// TabClient referring to the tab, and a ThreadClient referring to the +// thread. +function attachTestThread(aClient, aTitle, aCallback) { + attachTestTab(aClient, aTitle, function (aTabResponse, aTabClient) { + function onAttach(aResponse, aThreadClient) { + aCallback(aResponse, aTabClient, aThreadClient, aTabResponse); + } + aTabClient.attachThread({ + useSourceMaps: true, + autoBlackBox: true + }, onAttach); + }); +} + +// Attach to |aClient|'s tab whose title is |aTitle|, attach to the tab's +// thread, and then resume it. Pass |aCallback| the thread's response to +// the 'resume' packet, a TabClient for the tab, and a ThreadClient for the +// thread. +function attachTestTabAndResume(aClient, aTitle, aCallback = () => {}) { + return new Promise((resolve, reject) => { + attachTestThread(aClient, aTitle, function (aResponse, aTabClient, aThreadClient) { + aThreadClient.resume(function (aResponse) { + aCallback(aResponse, aTabClient, aThreadClient); + resolve([aResponse, aTabClient, aThreadClient]); + }); + }); + }); +} + +/** + * Initialize the testing debugger server. + */ +function initTestDebuggerServer(aServer = DebuggerServer) +{ + aServer.registerModule("xpcshell-test/testactors"); + // Allow incoming connections. + aServer.init(function () { return true; }); +} + +/** + * Initialize the testing debugger server with a tab whose title is |title|. + */ +function startTestDebuggerServer(title, server = DebuggerServer) { + initTestDebuggerServer(server); + addTestGlobal(title); + DebuggerServer.addTabActors(); + + let transport = DebuggerServer.connectPipe(); + let client = new DebuggerClient(transport); + + return connect(client).then(() => client); +} + +function finishClient(aClient) +{ + aClient.close(function () { + DebuggerServer.destroy(); + do_test_finished(); + }); +} + +// Create a server, connect to it and fetch tab actors for the parent process; +// pass |aCallback| the debugger client and tab actor form with all actor IDs. +function get_chrome_actors(callback) +{ + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + DebuggerServer.allowChromeProcess = true; + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect() + .then(() => client.getProcess()) + .then(response => { + callback(client, response.form); + }); +} + +function getChromeActors(client, server = DebuggerServer) { + server.allowChromeProcess = true; + return client.getProcess().then(response => response.form); +} + +/** + * Takes a relative file path and returns the absolute file url for it. + */ +function getFileUrl(aName, aAllowMissing = false) { + let file = do_get_file(aName, aAllowMissing); + return Services.io.newFileURI(file).spec; +} + +/** + * Returns the full path of the file with the specified name in a + * platform-independent and URL-like form. + */ +function getFilePath(aName, aAllowMissing = false, aUsePlatformPathSeparator = false) +{ + let file = do_get_file(aName, aAllowMissing); + let path = Services.io.newFileURI(file).spec; + let filePrePath = "file://"; + if ("nsILocalFileWin" in Ci && + file instanceof Ci.nsILocalFileWin) { + filePrePath += "/"; + } + + path = path.slice(filePrePath.length); + + if (aUsePlatformPathSeparator && path.match(/^\w:/)) { + path = path.replace(/\//g, "\\"); + } + + return path; +} + +/** + * Returns the full text contents of the given file. + */ +function readFile(aFileName) { + let f = do_get_file(aFileName); + let s = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + s.init(f, -1, -1, false); + try { + return NetUtil.readInputStreamToString(s, s.available()); + } finally { + s.close(); + } +} + +function writeFile(aFileName, aContent) { + let file = do_get_file(aFileName, true); + let stream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + stream.init(file, -1, -1, 0); + try { + do { + let numWritten = stream.write(aContent, aContent.length); + aContent = aContent.slice(numWritten); + } while (aContent.length > 0); + } finally { + stream.close(); + } +} + +function connectPipeTracing() { + return new TracingTransport(DebuggerServer.connectPipe()); +} + +function TracingTransport(childTransport) { + this.hooks = null; + this.child = childTransport; + this.child.hooks = this; + + this.expectations = []; + this.packets = []; + this.checkIndex = 0; +} + +TracingTransport.prototype = { + // Remove actor names + normalize: function (packet) { + return JSON.parse(JSON.stringify(packet, (key, value) => { + if (key === "to" || key === "from" || key === "actor") { + return ""; + } + return value; + })); + }, + send: function (packet) { + this.packets.push({ + type: "sent", + packet: this.normalize(packet) + }); + return this.child.send(packet); + }, + close: function () { + return this.child.close(); + }, + ready: function () { + return this.child.ready(); + }, + onPacket: function (packet) { + this.packets.push({ + type: "received", + packet: this.normalize(packet) + }); + this.hooks.onPacket(packet); + }, + onClosed: function () { + this.hooks.onClosed(); + }, + + expectSend: function (expected) { + let packet = this.packets[this.checkIndex++]; + do_check_eq(packet.type, "sent"); + deepEqual(packet.packet, this.normalize(expected)); + }, + + expectReceive: function (expected) { + let packet = this.packets[this.checkIndex++]; + do_check_eq(packet.type, "received"); + deepEqual(packet.packet, this.normalize(expected)); + }, + + // Write your tests, call dumpLog at the end, inspect the output, + // then sprinkle the calls through the right places in your test. + dumpLog: function () { + for (let entry of this.packets) { + if (entry.type === "sent") { + dumpn("trace.expectSend(" + entry.packet + ");"); + } else { + dumpn("trace.expectReceive(" + entry.packet + ");"); + } + } + } +}; + +function StubTransport() { } +StubTransport.prototype.ready = function () {}; +StubTransport.prototype.send = function () {}; +StubTransport.prototype.close = function () {}; + +function executeSoon(aFunc) { + Services.tm.mainThread.dispatch({ + run: DevToolsUtils.makeInfallible(aFunc) + }, Ci.nsIThread.DISPATCH_NORMAL); +} + +// The do_check_* family of functions expect their last argument to be an +// optional stack object. Unfortunately, most tests actually pass a in a string +// containing an error message instead, which causes error reporting to break if +// strict warnings as errors is turned on. To avoid this, we wrap these +// functions here below to ensure the correct number of arguments is passed. +// +// TODO: Remove this once bug 906232 is resolved +// +var do_check_true_old = do_check_true; +var do_check_true = function (condition) { + do_check_true_old(condition); +}; + +var do_check_false_old = do_check_false; +var do_check_false = function (condition) { + do_check_false_old(condition); +}; + +var do_check_eq_old = do_check_eq; +var do_check_eq = function (left, right) { + do_check_eq_old(left, right); +}; + +var do_check_neq_old = do_check_neq; +var do_check_neq = function (left, right) { + do_check_neq_old(left, right); +}; + +var do_check_matches_old = do_check_matches; +var do_check_matches = function (pattern, value) { + do_check_matches_old(pattern, value); +}; + +// Create async version of the object where calling each method +// is equivalent of calling it with asyncall. Mainly useful for +// destructuring objects with methods that take callbacks. +const Async = target => new Proxy(target, Async); +Async.get = (target, name) => + typeof (target[name]) === "function" ? asyncall.bind(null, target[name], target) : + target[name]; + +// Calls async function that takes callback and errorback and returns +// returns promise representing result. +const asyncall = (fn, self, ...args) => + new Promise((...etc) => fn.call(self, ...args, ...etc)); + +const Test = task => () => { + add_task(task); + run_next_test(); +}; + +const assert = do_check_true; + +/** + * Create a promise that is resolved on the next occurence of the given event. + * + * @param DebuggerClient client + * @param String event + * @param Function predicate + * @returns Promise + */ +function waitForEvent(client, type, predicate) { + return new Promise(function (resolve) { + function listener(type, packet) { + if (!predicate(packet)) { + return; + } + client.removeListener(listener); + resolve(packet); + } + + if (predicate) { + client.addListener(type, listener); + } else { + client.addOneTimeListener(type, function (type, packet) { + resolve(packet); + }); + } + }); +} + +/** + * Execute the action on the next tick and return a promise that is resolved on + * the next pause. + * + * When using promises and Task.jsm, we often want to do an action that causes a + * pause and continue the task once the pause has ocurred. Unfortunately, if we + * do the action that causes the pause within the task's current tick we will + * pause before we have a chance to yield the promise that waits for the pause + * and we enter a dead lock. The solution is to create the promise that waits + * for the pause, schedule the action to run on the next tick of the event loop, + * and finally yield the promise. + * + * @param Function action + * @param DebuggerClient client + * @returns Promise + */ +function executeOnNextTickAndWaitForPause(action, client) { + const paused = waitForPause(client); + executeSoon(action); + return paused; +} + +/** + * Interrupt JS execution for the specified thread. + * + * @param ThreadClient threadClient + * @returns Promise + */ +function interrupt(threadClient) { + dumpn("Interrupting."); + return threadClient.interrupt(); +} + +/** + * Resume JS execution for the specified thread and then wait for the next pause + * event. + * + * @param DebuggerClient client + * @param ThreadClient threadClient + * @returns Promise + */ +function resumeAndWaitForPause(client, threadClient) { + const paused = waitForPause(client); + return resume(threadClient).then(() => paused); +} + +/** + * Resume JS execution for a single step and wait for the pause after the step + * has been taken. + * + * @param DebuggerClient client + * @param ThreadClient threadClient + * @returns Promise + */ +function stepIn(client, threadClient) { + dumpn("Stepping in."); + const paused = waitForPause(client); + return threadClient.stepIn() + .then(() => paused); +} + +/** + * Resume JS execution for a step over and wait for the pause after the step + * has been taken. + * + * @param DebuggerClient client + * @param ThreadClient threadClient + * @returns Promise + */ +function stepOver(client, threadClient) { + dumpn("Stepping over."); + return threadClient.stepOver() + .then(() => waitForPause(client)); +} + +/** + * Get the list of `count` frames currently on stack, starting at the index + * `first` for the specified thread. + * + * @param ThreadClient threadClient + * @param Number first + * @param Number count + * @returns Promise + */ +function getFrames(threadClient, first, count) { + dumpn("Getting frames."); + return threadClient.getFrames(first, count); +} + +/** + * Black box the specified source. + * + * @param SourceClient sourceClient + * @returns Promise + */ +function blackBox(sourceClient) { + dumpn("Black boxing source: " + sourceClient.actor); + return sourceClient.blackBox(); +} + +/** + * Stop black boxing the specified source. + * + * @param SourceClient sourceClient + * @returns Promise + */ +function unBlackBox(sourceClient) { + dumpn("Un-black boxing source: " + sourceClient.actor); + return sourceClient.unblackBox(); +} + +/** + * Perform a "source" RDP request with the given SourceClient to get the source + * content and content type. + * + * @param SourceClient sourceClient + * @returns Promise + */ +function getSourceContent(sourceClient) { + dumpn("Getting source content for " + sourceClient.actor); + return sourceClient.source(); +} + +/** + * Get a source at the specified url. + * + * @param ThreadClient threadClient + * @param string url + * @returns Promise + */ +function getSource(threadClient, url) { + let deferred = promise.defer(); + threadClient.getSources((res) => { + let source = res.sources.filter(function (s) { + return s.url === url; + }); + if (source.length) { + deferred.resolve(threadClient.source(source[0])); + } + else { + deferred.reject(new Error("source not found")); + } + }); + return deferred.promise; +} + +/** + * Do a fake reload which clears the thread debugger + * + * @param TabClient tabClient + * @returns Promise + */ +function reload(tabClient) { + let deferred = promise.defer(); + tabClient._reload({}, deferred.resolve); + return deferred.promise; +} + +/** + * Returns an array of stack location strings given a thread and a sample. + * + * @param object thread + * @param object sample + * @returns object + */ +function getInflatedStackLocations(thread, sample) { + let stackTable = thread.stackTable; + let frameTable = thread.frameTable; + let stringTable = thread.stringTable; + let SAMPLE_STACK_SLOT = thread.samples.schema.stack; + let STACK_PREFIX_SLOT = stackTable.schema.prefix; + let STACK_FRAME_SLOT = stackTable.schema.frame; + let FRAME_LOCATION_SLOT = frameTable.schema.location; + + // Build the stack from the raw data and accumulate the locations in + // an array. + let stackIndex = sample[SAMPLE_STACK_SLOT]; + let locations = []; + while (stackIndex !== null) { + let stackEntry = stackTable.data[stackIndex]; + let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]]; + locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]); + stackIndex = stackEntry[STACK_PREFIX_SLOT]; + } + + // The profiler tree is inverted, so reverse the array. + return locations.reverse(); +} diff --git a/devtools/server/tests/unit/hello-actor.js b/devtools/server/tests/unit/hello-actor.js new file mode 100644 index 000000000..6d7427f63 --- /dev/null +++ b/devtools/server/tests/unit/hello-actor.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const protocol = require("devtools/shared/protocol"); + +const helloSpec = protocol.generateActorSpec({ + typeName: "helloActor", + + methods: { + hello: {} + } +}); + +var HelloActor = protocol.ActorClassWithSpec(helloSpec, { + hello: function () { + return; + } +}); diff --git a/devtools/server/tests/unit/post_init_global_actors.js b/devtools/server/tests/unit/post_init_global_actors.js new file mode 100644 index 000000000..0035e8914 --- /dev/null +++ b/devtools/server/tests/unit/post_init_global_actors.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function PostInitGlobalActor(aConnection) {} + +PostInitGlobalActor.prototype = { + actorPrefix: "postInitGlobal", + onPing: function onPing(aRequest) { + return { message: "pong" }; + }, +}; + +PostInitGlobalActor.prototype.requestTypes = { + "ping": PostInitGlobalActor.prototype.onPing, +}; + +DebuggerServer.addGlobalActor(PostInitGlobalActor, "postInitGlobalActor"); diff --git a/devtools/server/tests/unit/post_init_tab_actors.js b/devtools/server/tests/unit/post_init_tab_actors.js new file mode 100644 index 000000000..9b9ddf111 --- /dev/null +++ b/devtools/server/tests/unit/post_init_tab_actors.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function PostInitTabActor(aConnection) {} + +PostInitTabActor.prototype = { + actorPostfix: "postInitTab", + onPing: function onPing(aRequest) { + return { message: "pong" }; + }, +}; + +PostInitTabActor.prototype.requestTypes = { + "ping": PostInitTabActor.prototype.onPing, +}; + +DebuggerServer.addGlobalActor(PostInitTabActor, "postInitTabActor"); diff --git a/devtools/server/tests/unit/pre_init_global_actors.js b/devtools/server/tests/unit/pre_init_global_actors.js new file mode 100644 index 000000000..bd4284a70 --- /dev/null +++ b/devtools/server/tests/unit/pre_init_global_actors.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function PreInitGlobalActor(aConnection) {} + +PreInitGlobalActor.prototype = { + actorPrefix: "preInitGlobal", + onPing: function onPing(aRequest) { + return { message: "pong" }; + }, +}; + +PreInitGlobalActor.prototype.requestTypes = { + "ping": PreInitGlobalActor.prototype.onPing, +}; + +DebuggerServer.addGlobalActor(PreInitGlobalActor, "preInitGlobalActor"); diff --git a/devtools/server/tests/unit/pre_init_tab_actors.js b/devtools/server/tests/unit/pre_init_tab_actors.js new file mode 100644 index 000000000..628f0fb2f --- /dev/null +++ b/devtools/server/tests/unit/pre_init_tab_actors.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function PreInitTabActor(aConnection) {} + +PreInitTabActor.prototype = { + actorPrefix: "preInitTab", + onPing: function onPing(aRequest) { + return { message: "pong" }; + }, +}; + +PreInitTabActor.prototype.requestTypes = { + "ping": PreInitTabActor.prototype.onPing, +}; + +DebuggerServer.addGlobalActor(PreInitTabActor, "preInitTabActor"); diff --git a/devtools/server/tests/unit/registertestactors-01.js b/devtools/server/tests/unit/registertestactors-01.js new file mode 100644 index 000000000..92f511225 --- /dev/null +++ b/devtools/server/tests/unit/registertestactors-01.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function Actor() {} + +exports.register = function (handle) { + handle.addTabActor(Actor, "registeredActor1"); + handle.addGlobalActor(Actor, "registeredActor1"); +}; + +exports.unregister = function (handle) { + handle.removeTabActor(Actor); + handle.removeGlobalActor(Actor); +}; + diff --git a/devtools/server/tests/unit/registertestactors-02.js b/devtools/server/tests/unit/registertestactors-02.js new file mode 100644 index 000000000..54f78e508 --- /dev/null +++ b/devtools/server/tests/unit/registertestactors-02.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function Actor() {} + +exports.register = function (handle) { + handle.addGlobalActor(Actor, "registeredActor2"); + handle.addTabActor(Actor, "registeredActor2"); +}; + +exports.unregister = function (handle) { + handle.removeTabActor(Actor); + handle.removeGlobalActor(Actor); +}; + diff --git a/devtools/server/tests/unit/registertestactors-03.js b/devtools/server/tests/unit/registertestactors-03.js new file mode 100644 index 000000000..8d42fdbd8 --- /dev/null +++ b/devtools/server/tests/unit/registertestactors-03.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {method, RetVal, Actor, ActorClassWithSpec, Front, FrontClassWithSpec, + generateActorSpec} = require("devtools/shared/protocol"); +var Services = require("Services"); + +const lazySpec = generateActorSpec({ + typeName: "lazy", + + methods: { + hello: { + response: { str: RetVal("string") } + } + } +}); + +exports.LazyActor = ActorClassWithSpec(lazySpec, { + initialize: function (conn, id) { + Actor.prototype.initialize.call(this, conn); + + Services.obs.notifyObservers(null, "actor", "instantiated"); + }, + + hello: function (str) { + return "world"; + } +}); + +Services.obs.notifyObservers(null, "actor", "loaded"); + +exports.LazyFront = FrontClassWithSpec(lazySpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client); + this.actorID = form.lazyActor; + + client.addActorPool(this); + this.manage(this); + } +}); diff --git a/devtools/server/tests/unit/setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/unit/setBreakpoint-on-column-in-gcd-script.js new file mode 100644 index 000000000..575915c4f --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-column-in-gcd-script.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; var b = 2; var c = 3; +})(); diff --git a/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-at-end-of-line.js b/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-at-end-of-line.js new file mode 100644 index 000000000..4c1b52eb4 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-at-end-of-line.js @@ -0,0 +1,6 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; + var c = 3; +} diff --git a/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js b/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js new file mode 100644 index 000000000..adce39193 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; var c = 3; +})(); diff --git a/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets.js b/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets.js new file mode 100644 index 000000000..5faefc3c8 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-column-with-no-offsets.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var c = 3; +} diff --git a/devtools/server/tests/unit/setBreakpoint-on-column.js b/devtools/server/tests/unit/setBreakpoint-on-column.js new file mode 100644 index 000000000..d92231e65 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-column.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; var c = 3; +} diff --git a/devtools/server/tests/unit/setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/unit/setBreakpoint-on-line-in-gcd-script.js new file mode 100644 index 000000000..fb96be8ab --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-line-in-gcd-script.js @@ -0,0 +1,9 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; + var b = 2; + var c = 3; +})(); diff --git a/devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-offsets.js new file mode 100644 index 000000000..b30ebb504 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-offsets.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + for (var i = 0; i < 1; ++i) { + ; + } +} diff --git a/devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-statements.js new file mode 100644 index 000000000..d92231e65 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-line-with-multiple-statements.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; var c = 3; +} diff --git a/devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js new file mode 100644 index 000000000..b03d40079 --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js @@ -0,0 +1,9 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; + + var c = 3; +})(); diff --git a/devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets.js new file mode 100644 index 000000000..1268cf8db --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-line-with-no-offsets.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + var a = 1; + + var c = 3; +} diff --git a/devtools/server/tests/unit/setBreakpoint-on-line.js b/devtools/server/tests/unit/setBreakpoint-on-line.js new file mode 100644 index 000000000..1b15e2a5e --- /dev/null +++ b/devtools/server/tests/unit/setBreakpoint-on-line.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + var a = 1; + var b = 2; + var c = 3; +} diff --git a/devtools/server/tests/unit/source-map-data/sourcemapped.coffee b/devtools/server/tests/unit/source-map-data/sourcemapped.coffee new file mode 100644 index 000000000..73a400a21 --- /dev/null +++ b/devtools/server/tests/unit/source-map-data/sourcemapped.coffee @@ -0,0 +1,6 @@ +foo = (n) -> + return "foo" + i for i in [0...n] + +[first, second, third] = foo(3) + +debugger \ No newline at end of file diff --git a/devtools/server/tests/unit/source-map-data/sourcemapped.map b/devtools/server/tests/unit/source-map-data/sourcemapped.map new file mode 100644 index 000000000..dcee3c33c --- /dev/null +++ b/devtools/server/tests/unit/source-map-data/sourcemapped.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "file": "sourcemapped.js", + "sourceRoot": "", + "sources": [ + "sourcemapped.coffee" + ], + "names": [], + "mappings": ";AAAA;CAAA,KAAA,yBAAA;CAAA;CAAA,CAAA,CAAA,MAAO;CACL,IAAA,GAAA;AAAA,CAAA,EAAA,MAA0B,qDAA1B;CAAA,EAAe,EAAR,QAAA;CAAP,IADI;CAAN,EAAM;;CAAN,CAGA,CAAyB,IAAA;;CAEzB,UALA;CAAA" +} \ No newline at end of file diff --git a/devtools/server/tests/unit/sourcemapped.js b/devtools/server/tests/unit/sourcemapped.js new file mode 100644 index 000000000..94d130903 --- /dev/null +++ b/devtools/server/tests/unit/sourcemapped.js @@ -0,0 +1,16 @@ +// Generated by CoffeeScript 1.6.1 +(function () { + var first, foo, second, third, _ref; + + foo = function (n) { + var i, _i; + for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) { + return "foo" + i; + } + }; + + _ref = foo(3), first = _ref[0], second = _ref[1], third = _ref[2]; + + debugger; + +}).call(this); diff --git a/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_01.js b/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_01.js new file mode 100644 index 000000000..2fd38d49c --- /dev/null +++ b/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_01.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can tell the memory actor to take a heap snapshot over the RDP +// and then create a HeapSnapshot instance from the resulting file. + +const { OS } = require("resource://gre/modules/osfile.jsm"); + +const run_test = makeMemoryActorTest(function* (client, memoryFront) { + const snapshotFilePath = yield memoryFront.saveHeapSnapshot(); + ok(!!(yield OS.File.stat(snapshotFilePath)), + "Should have the heap snapshot file"); + const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(snapshotFilePath); + ok(snapshot instanceof HeapSnapshot, + "And we should be able to read a HeapSnapshot instance from the file"); +}); diff --git a/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_02.js b/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_02.js new file mode 100644 index 000000000..564ec0d06 --- /dev/null +++ b/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_02.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can properly stream heap snapshot files over the RDP as bulk +// data. + +const { OS } = require("resource://gre/modules/osfile.jsm"); + +const run_test = makeMemoryActorTest(function* (client, memoryFront) { + const snapshotFilePath = yield memoryFront.saveHeapSnapshot({ + forceCopy: true + }); + ok(!!(yield OS.File.stat(snapshotFilePath)), + "Should have the heap snapshot file"); + const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(snapshotFilePath); + ok(snapshot instanceof HeapSnapshot, + "And we should be able to read a HeapSnapshot instance from the file"); +}); diff --git a/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_03.js b/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_03.js new file mode 100644 index 000000000..e9e81594d --- /dev/null +++ b/devtools/server/tests/unit/test_MemoryActor_saveHeapSnapshot_03.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can save full runtime heap snapshots when attached to the +// ChromeActor or a ChildProcessActor. + +const { OS } = require("resource://gre/modules/osfile.jsm"); + +const run_test = makeFullRuntimeMemoryActorTest(function* (client, memoryFront) { + const snapshotFilePath = yield memoryFront.saveHeapSnapshot(); + ok(!!(yield OS.File.stat(snapshotFilePath)), + "Should have the heap snapshot file"); + const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(snapshotFilePath); + ok(snapshot instanceof HeapSnapshot, + "And we should be able to read a HeapSnapshot instance from the file"); +}); diff --git a/devtools/server/tests/unit/test_actor-registry-actor.js b/devtools/server/tests/unit/test_actor-registry-actor.js new file mode 100644 index 000000000..8b0abfbbb --- /dev/null +++ b/devtools/server/tests/unit/test_actor-registry-actor.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that you can register new actors via the ActorRegistrationActor. + */ + +var gClient; +var gRegistryFront; +var gActorFront; +var gOldPref; + +const { ActorRegistryFront } = require("devtools/shared/fronts/actor-registry"); + +function run_test() +{ + gOldPref = Services.prefs.getBoolPref("devtools.debugger.forbid-certified-apps"); + Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", false); + initTestDebuggerServer(); + DebuggerServer.addBrowserActors(); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(getRegistry); + do_test_pending(); +} + +function getRegistry() { + gClient.listTabs((response) => { + gRegistryFront = ActorRegistryFront(gClient, response); + registerNewActor(); + }); +} + +function registerNewActor() { + let options = { + prefix: "helloActor", + constructor: "HelloActor", + type: { global: true } + }; + + gRegistryFront + .registerActor("resource://test/hello-actor.js", options) + .then(actorFront => gActorFront = actorFront) + .then(talkToNewActor) + .then(null, e => { + DevToolsUtils.reportException("registerNewActor", e); + do_check_true(false); + }); +} + +function talkToNewActor() { + gClient.listTabs(({ helloActor }) => { + do_check_true(!!helloActor); + gClient.request({ + to: helloActor, + type: "hello" + }, response => { + do_check_true(!response.error); + unregisterNewActor(); + }); + }); +} + +function unregisterNewActor() { + gActorFront + .unregister() + .then(testActorIsUnregistered) + .then(null, e => { + DevToolsUtils.reportException("unregisterNewActor", e); + do_check_true(false); + }); +} + +function testActorIsUnregistered() { + gClient.listTabs(({ helloActor }) => { + do_check_true(!helloActor); + + Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", gOldPref); + finishClient(gClient); + }); +} diff --git a/devtools/server/tests/unit/test_add_actors.js b/devtools/server/tests/unit/test_add_actors.js new file mode 100644 index 000000000..9b90da724 --- /dev/null +++ b/devtools/server/tests/unit/test_add_actors.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gClient; +var gActors; + +/** + * The purpose of these tests is to verify that it's possible to add actors + * both before and after the DebuggerServer has been initialized, so addons + * that add actors don't have to poll the object for its initialization state + * in order to add actors after initialization but rather can add actors anytime + * regardless of the object's state. + */ +function run_test() +{ + DebuggerServer.addActors("resource://test/pre_init_global_actors.js"); + DebuggerServer.addActors("resource://test/pre_init_tab_actors.js"); + + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + DebuggerServer.addActors("resource://test/post_init_global_actors.js"); + DebuggerServer.addActors("resource://test/post_init_tab_actors.js"); + + add_test(init); + add_test(test_pre_init_global_actor); + add_test(test_pre_init_tab_actor); + add_test(test_post_init_global_actor); + add_test(test_post_init_tab_actor); + add_test(test_stable_global_actor_instances); + add_test(close_client); + run_next_test(); +} + +function init() +{ + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect() + .then(() => gClient.listTabs()) + .then(aResponse => { + gActors = aResponse; + run_next_test(); + }); +} + +function test_pre_init_global_actor() +{ + gClient.request({ to: gActors.preInitGlobalActor, type: "ping" }, + function onResponse(aResponse) { + do_check_eq(aResponse.message, "pong"); + run_next_test(); + } + ); +} + +function test_pre_init_tab_actor() +{ + gClient.request({ to: gActors.preInitTabActor, type: "ping" }, + function onResponse(aResponse) { + do_check_eq(aResponse.message, "pong"); + run_next_test(); + } + ); +} + +function test_post_init_global_actor() +{ + gClient.request({ to: gActors.postInitGlobalActor, type: "ping" }, + function onResponse(aResponse) { + do_check_eq(aResponse.message, "pong"); + run_next_test(); + } + ); +} + +function test_post_init_tab_actor() +{ + gClient.request({ to: gActors.postInitTabActor, type: "ping" }, + function onResponse(aResponse) { + do_check_eq(aResponse.message, "pong"); + run_next_test(); + } + ); +} + +// Get the object object, from the server side, for a given actor ID +function getActorInstance(connID, actorID) { + return DebuggerServer._connections[connID].getActor(actorID); +} + +function test_stable_global_actor_instances() +{ + // Consider that there is only one connection, + // and the first one is ours + let connID = Object.keys(DebuggerServer._connections)[0]; + let postInitGlobalActor = getActorInstance(connID, gActors.postInitGlobalActor); + let preInitGlobalActor = getActorInstance(connID, gActors.preInitGlobalActor); + gClient.listTabs(function onListTabs(aResponse) { + do_check_eq(postInitGlobalActor, getActorInstance(connID, aResponse.postInitGlobalActor)); + do_check_eq(preInitGlobalActor, getActorInstance(connID, aResponse.preInitGlobalActor)); + run_next_test(); + }); +} + +function close_client() { + gClient.close().then(() => run_next_test()); +} diff --git a/devtools/server/tests/unit/test_addon_reload.js b/devtools/server/tests/unit/test_addon_reload.js new file mode 100644 index 000000000..0187fa3b3 --- /dev/null +++ b/devtools/server/tests/unit/test_addon_reload.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const protocol = require("devtools/shared/protocol"); +const {AddonManager} = require("resource://gre/modules/AddonManager.jsm"); + +startupAddonsManager(); + +function promiseAddonEvent(event) { + return new Promise(resolve => { + let listener = { + [event]: function (...args) { + AddonManager.removeAddonListener(listener); + resolve(args); + } + }; + + AddonManager.addAddonListener(listener); + }); +} + +function* findAddonInRootList(client, addonId) { + const result = yield client.listAddons(); + const addonActor = result.addons.filter(addon => addon.id === addonId)[0]; + ok(addonActor, `Found add-on actor for ${addonId}`); + return addonActor; +} + +function* reloadAddon(client, addonActor) { + // The add-on will be re-installed after a successful reload. + const onInstalled = promiseAddonEvent("onInstalled"); + yield client.request({to: addonActor.actor, type: "reload"}); + yield onInstalled; +} + +function getSupportFile(path) { + const allowMissing = false; + return do_get_file(path, allowMissing); +} + +add_task(function* testReloadExitedAddon() { + const client = yield new Promise(resolve => { + get_chrome_actors(client => resolve(client)); + }); + + // Install our main add-on to trigger reloads on. + const addonFile = getSupportFile("addons/web-extension"); + const installedAddon = yield AddonManager.installTemporaryAddon( + addonFile); + + // Install a decoy add-on. + const addonFile2 = getSupportFile("addons/web-extension2"); + const installedAddon2 = yield AddonManager.installTemporaryAddon( + addonFile2); + + let addonActor = yield findAddonInRootList(client, installedAddon.id); + + yield reloadAddon(client, addonActor); + + // Uninstall the decoy add-on, which should cause its actor to exit. + const onUninstalled = promiseAddonEvent("onUninstalled"); + installedAddon2.uninstall(); + const [uninstalledAddon] = yield onUninstalled; + + // Try to re-list all add-ons after a reload. + // This was throwing an exception because of the exited actor. + const newAddonActor = yield findAddonInRootList(client, installedAddon.id); + equal(newAddonActor.id, addonActor.id); + + // The actor id should be the same after the reload + equal(newAddonActor.actor, addonActor.actor); + + const onAddonListChanged = new Promise((resolve) => { + client.addListener("addonListChanged", function listener() { + client.removeListener("addonListChanged", listener); + resolve(); + }); + }); + + // Install an upgrade version of the first add-on. + const addonUpgradeFile = getSupportFile("addons/web-extension-upgrade"); + const upgradedAddon = yield AddonManager.installTemporaryAddon( + addonUpgradeFile); + + // Waiting for addonListChanged unsolicited event + yield onAddonListChanged; + + // re-list all add-ons after an upgrade. + const upgradedAddonActor = yield findAddonInRootList(client, upgradedAddon.id); + equal(upgradedAddonActor.id, addonActor.id); + // The actor id should be the same after the upgrade. + equal(upgradedAddonActor.actor, addonActor.actor); + + // The addon metadata has been updated. + equal(upgradedAddonActor.name, "Test Addons Actor Upgrade"); + + yield close(client); +}); diff --git a/devtools/server/tests/unit/test_addons_actor.js b/devtools/server/tests/unit/test_addons_actor.js new file mode 100644 index 000000000..1815d43c6 --- /dev/null +++ b/devtools/server/tests/unit/test_addons_actor.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const protocol = require("devtools/shared/protocol"); +const {AddonsActor} = require("devtools/server/actors/addons"); +const {AddonsFront} = require("devtools/shared/fronts/addons"); + +startupAddonsManager(); + +function* connect() { + const client = yield new Promise(resolve => { + get_chrome_actors(client => resolve(client)); + }); + const root = yield listTabs(client); + const addonsActor = root.addonsActor; + ok(addonsActor, "Got AddonsActor instance"); + + const addons = AddonsFront(client, {addonsActor}); + return [client, addons]; +} + +add_task(function* testSuccessfulInstall() { + const [client, addons] = yield connect(); + + const allowMissing = false; + const usePlatformSeparator = true; + const addonPath = getFilePath("addons/web-extension", + allowMissing, usePlatformSeparator); + const installedAddon = yield addons.installTemporaryAddon(addonPath); + equal(installedAddon.id, "test-addons-actor@mozilla.org"); + // The returned object is currently not a proper actor. + equal(installedAddon.actor, false); + + const addonList = yield client.listAddons(); + ok(addonList && addonList.addons && addonList.addons.map(a => a.name), + "Received list of add-ons"); + const addon = addonList.addons.filter(a => a.id === installedAddon.id)[0]; + ok(addon, "Test add-on appeared in root install list"); + + yield close(client); +}); + +add_task(function* testNonExistantPath() { + const [client, addons] = yield connect(); + + yield Assert.rejects( + addons.installTemporaryAddon("some-non-existant-path"), + /Could not install add-on.*Component returned failure/); + + yield close(client); +}); diff --git a/devtools/server/tests/unit/test_animation_name.js b/devtools/server/tests/unit/test_animation_name.js new file mode 100644 index 000000000..4cd708fc4 --- /dev/null +++ b/devtools/server/tests/unit/test_animation_name.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that AnimationPlayerActor.getName returns the right name depending on +// the type of an animation and the various properties available on it. + +const { AnimationPlayerActor } = require("devtools/server/actors/animation"); + +function run_test() { + // Mock a window with just the properties the AnimationPlayerActor uses. + let window = { + MutationObserver: function () { + this.observe = () => {}; + }, + Animation: function () { + this.effect = {target: getMockNode()}; + }, + CSSAnimation: function () { + this.effect = {target: getMockNode()}; + }, + CSSTransition: function () { + this.effect = {target: getMockNode()}; + } + }; + + window.CSSAnimation.prototype = Object.create(window.Animation.prototype); + window.CSSTransition.prototype = Object.create(window.Animation.prototype); + + // Helper to get a mock DOM node. + function getMockNode() { + return { + ownerDocument: { + defaultView: window + } + }; + } + + // Objects in this array should contain the following properties: + // - desc {String} For logging + // - animation {Object} An animation object instantiated from one of the mock + // window animation constructors. + // - props {Objet} Properties of this object will be added to the animation + // object. + // - expectedName {String} The expected name returned by + // AnimationPlayerActor.getName. + const TEST_DATA = [{ + desc: "Animation with an id", + animation: new window.Animation(), + props: { id: "animation-id" }, + expectedName: "animation-id" + }, { + desc: "Animation without an id", + animation: new window.Animation(), + props: {}, + expectedName: "" + }, { + desc: "CSSTransition with an id", + animation: new window.CSSTransition(), + props: { id: "transition-with-id", transitionProperty: "width" }, + expectedName: "transition-with-id" + }, { + desc: "CSSAnimation with an id", + animation: new window.CSSAnimation(), + props: { id: "animation-with-id", animationName: "move" }, + expectedName: "animation-with-id" + }, { + desc: "CSSTransition without an id", + animation: new window.CSSTransition(), + props: { transitionProperty: "width" }, + expectedName: "width" + }, { + desc: "CSSAnimation without an id", + animation: new window.CSSAnimation(), + props: { animationName: "move" }, + expectedName: "move" + }]; + + for (let { desc, animation, props, expectedName } of TEST_DATA) { + do_print(desc); + for (let key in props) { + animation[key] = props[key]; + } + let actor = AnimationPlayerActor({}, animation); + do_check_eq(actor.getName(), expectedName); + } +} diff --git a/devtools/server/tests/unit/test_animation_type.js b/devtools/server/tests/unit/test_animation_type.js new file mode 100644 index 000000000..0f37755a4 --- /dev/null +++ b/devtools/server/tests/unit/test_animation_type.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the output of AnimationPlayerActor.getType(). + +const { ANIMATION_TYPES, AnimationPlayerActor } = + require("devtools/server/actors/animation"); + +function run_test() { + // Mock a window with just the properties the AnimationPlayerActor uses. + let window = { + MutationObserver: function () { + this.observe = () => {}; + }, + Animation: function () { + this.effect = {target: getMockNode()}; + }, + CSSAnimation: function () { + this.effect = {target: getMockNode()}; + }, + CSSTransition: function () { + this.effect = {target: getMockNode()}; + } + }; + + window.CSSAnimation.prototype = Object.create(window.Animation.prototype); + window.CSSTransition.prototype = Object.create(window.Animation.prototype); + + // Helper to get a mock DOM node. + function getMockNode() { + return { + ownerDocument: { + defaultView: window + } + }; + } + + // Objects in this array should contain the following properties: + // - desc {String} For logging + // - animation {Object} An animation object instantiated from one of the mock + // window animation constructors. + // - expectedType {String} The expected type returned by + // AnimationPlayerActor.getType. + const TEST_DATA = [{ + desc: "Test CSSAnimation type", + animation: new window.CSSAnimation(), + expectedType: ANIMATION_TYPES.CSS_ANIMATION + }, { + desc: "Test CSSTransition type", + animation: new window.CSSTransition(), + expectedType: ANIMATION_TYPES.CSS_TRANSITION + }, { + desc: "Test ScriptAnimation type", + animation: new window.Animation(), + expectedType: ANIMATION_TYPES.SCRIPT_ANIMATION + }, { + desc: "Test unknown type", + animation: {effect: {target: getMockNode()}}, + expectedType: ANIMATION_TYPES.UNKNOWN + }]; + + for (let { desc, animation, expectedType } of TEST_DATA) { + do_print(desc); + let actor = AnimationPlayerActor({}, animation); + do_check_eq(actor.getType(), expectedType); + } +} diff --git a/devtools/server/tests/unit/test_attach.js b/devtools/server/tests/unit/test_attach.js new file mode 100644 index 000000000..a69db2c2b --- /dev/null +++ b/devtools/server/tests/unit/test_attach.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gClient; +var gDebuggee; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = testGlobal("test-1"); + DebuggerServer.addTestGlobal(gDebuggee); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect().then(function ([aType, aTraits]) { + attachTestTab(gClient, "test-1", function (aReply, aTabClient) { + test_attach(aTabClient); + }); + }); + do_test_pending(); +} + +function test_attach(aTabClient) +{ + aTabClient.attachThread({}, function (aResponse, aThreadClient) { + do_check_eq(aThreadClient.state, "paused"); + aThreadClient.resume(cleanup); + }); +} + +function cleanup() +{ + gClient.addListener("closed", function (aEvent) { + do_test_finished(); + }); + gClient.close(); +} diff --git a/devtools/server/tests/unit/test_blackboxing-01.js b/devtools/server/tests/unit/test_blackboxing-01.js new file mode 100644 index 000000000..d5356c390 --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-01.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test basic black boxing. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + testBlackBox(); + }); + }); + do_test_pending(); +} + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +const testBlackBox = Task.async(function* () { + let packet = yield executeOnNextTickAndWaitForPause(evalCode, gClient); + let source = gThreadClient.source(packet.frame.where.source); + + yield setBreakpoint(source, { + line: 2 + }); + yield resume(gThreadClient); + + const { sources } = yield getSources(gThreadClient); + let sourceClient = gThreadClient.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0]); + do_check_true(!sourceClient.isBlackBoxed, + "By default the source is not black boxed."); + + // Test that we can step into `doStuff` when we are not black boxed. + yield runTest( + function onSteppedLocation(aLocation) { + do_check_eq(aLocation.source.url, BLACK_BOXED_URL); + do_check_eq(aLocation.line, 2); + }, + function onDebuggerStatementFrames(aFrames) { + do_check_true(!aFrames.some(f => f.where.source.isBlackBoxed)); + } + ); + + let blackBoxResponse = yield blackBox(sourceClient); + do_check_true(sourceClient.isBlackBoxed); + + // Test that we step through `doStuff` when we are black boxed and its frame + // doesn't show up. + yield runTest( + function onSteppedLocation(aLocation) { + do_check_eq(aLocation.source.url, SOURCE_URL); + do_check_eq(aLocation.line, 4); + }, + function onDebuggerStatementFrames(aFrames) { + for (let f of aFrames) { + if (f.where.source.url == BLACK_BOXED_URL) { + do_check_true(f.where.source.isBlackBoxed); + } else { + do_check_true(!f.where.source.isBlackBoxed); + } + } + } + ); + + let unBlackBoxResponse = yield unBlackBox(sourceClient); + do_check_true(!sourceClient.isBlackBoxed); + + // Test that we can step into `doStuff` again. + yield runTest( + function onSteppedLocation(aLocation) { + do_check_eq(aLocation.source.url, BLACK_BOXED_URL); + do_check_eq(aLocation.line, 2); + }, + function onDebuggerStatementFrames(aFrames) { + do_check_true(!aFrames.some(f => f.where.source.isBlackBoxed)); + } + ); + + finishClient(gClient); +}); + +function evalCode() { + Components.utils.evalInSandbox( + "" + function doStuff(k) { // line 1 + let arg = 15; // line 2 - Step in here + k(arg); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + Components.utils.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 - Break here + function (n) { // line 3 - Step through `doStuff` to here + debugger; // line 4 + } // line 5 + ); // line 6 + } + "\n" // line 7 + + "debugger;", // line 8 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +const runTest = Task.async(function* (onSteppedLocation, onDebuggerStatementFrames) { + let packet = yield executeOnNextTickAndWaitForPause(gDebuggee.runTest, + gClient); + do_check_eq(packet.why.type, "breakpoint"); + + yield stepIn(gClient, gThreadClient); + yield stepIn(gClient, gThreadClient); + yield stepIn(gClient, gThreadClient); + + const location = yield getCurrentLocation(); + onSteppedLocation(location); + + packet = yield resumeAndWaitForPause(gClient, gThreadClient); + do_check_eq(packet.why.type, "debuggerStatement"); + + let { frames } = yield getFrames(gThreadClient, 0, 100); + onDebuggerStatementFrames(frames); + + return resume(gThreadClient); +}); + +const getCurrentLocation = Task.async(function* () { + const response = yield getFrames(gThreadClient, 0, 1); + return response.frames[0].where; +}); diff --git a/devtools/server/tests/unit/test_blackboxing-02.js b/devtools/server/tests/unit/test_blackboxing-02.js new file mode 100644 index 000000000..5bfb3641a --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-02.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we don't hit breakpoints in black boxed sources, and that when we + * unblack box the source again, the breakpoint hasn't disappeared and we will + * hit it again. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_black_box(); + }); + }); + do_test_pending(); +} + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function test_black_box() +{ + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.eval(aPacket.frame.actor, "doStuff", function (aResponse) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let obj = gThreadClient.pauseGrip(aPacket.why.frameFinished.return); + obj.getDefinitionSite(runWithSource); + }); + }); + + function runWithSource(aPacket) { + let source = gThreadClient.source(aPacket.source); + source.setBreakpoint({ + line: 2 + }, function (aResponse) { + do_check_true(!aResponse.error, "Should be able to set breakpoint."); + gThreadClient.resume(test_black_box_breakpoint); + }); + } + }); + + Components.utils.evalInSandbox( + "" + function doStuff(k) { // line 1 + let arg = 15; // line 2 - Break here + k(arg); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + Components.utils.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function (n) { // line 3 + debugger; // line 5 + } // line 6 + ); // line 7 + } // line 8 + + "\n debugger;", // line 9 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +function test_black_box_breakpoint() { + gThreadClient.getSources(function ({error, sources}) { + do_check_true(!error, "Should not get an error: " + error); + let sourceClient = gThreadClient.source(sources.filter(s => s.url == BLACK_BOXED_URL)[0]); + sourceClient.blackBox(function ({error}) { + do_check_true(!error, "Should not get an error: " + error); + + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "debuggerStatement", + "We should pass over the breakpoint since the source is black boxed."); + gThreadClient.resume(test_unblack_box_breakpoint.bind(null, sourceClient)); + }); + gDebuggee.runTest(); + }); + }); +} + +function test_unblack_box_breakpoint(aSourceClient) { + aSourceClient.unblackBox(function ({error}) { + do_check_true(!error, "Should not get an error: " + error); + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "breakpoint", + "We should hit the breakpoint again"); + + // We will hit the debugger statement on resume, so do this nastiness to skip over it. + gClient.addOneTimeListener( + "paused", + gThreadClient.resume.bind( + gThreadClient, + finishClient.bind(null, gClient))); + gThreadClient.resume(); + }); + gDebuggee.runTest(); + }); +} diff --git a/devtools/server/tests/unit/test_blackboxing-03.js b/devtools/server/tests/unit/test_blackboxing-03.js new file mode 100644 index 000000000..48f178777 --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-03.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we don't stop at debugger statements inside black boxed sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gBpClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_black_box(); + }); + }); + do_test_pending(); +} + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function test_black_box() +{ + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + source.setBreakpoint({ + line: 4 + }, function ({error}, bpClient) { + gBpClient = bpClient; + do_check_true(!error, "Should not get an error: " + error); + gThreadClient.resume(test_black_box_dbg_statement); + }); + }); + + Components.utils.evalInSandbox( + "" + function doStuff(k) { // line 1 + debugger; // line 2 - Break here + k(100); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + Components.utils.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function (n) { // line 3 + Math.abs(n); // line 4 - Break here + } // line 5 + ); // line 6 + } // line 7 + + "\n debugger;", // line 8 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +function test_black_box_dbg_statement() { + gThreadClient.getSources(function ({error, sources}) { + do_check_true(!error, "Should not get an error: " + error); + let sourceClient = gThreadClient.source(sources.filter(s => s.url == BLACK_BOXED_URL)[0]); + + sourceClient.blackBox(function ({error}) { + do_check_true(!error, "Should not get an error: " + error); + + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "breakpoint", + "We should pass over the debugger statement."); + gBpClient.remove(function ({error}) { + do_check_true(!error, "Should not get an error: " + error); + gThreadClient.resume(test_unblack_box_dbg_statement.bind(null, sourceClient)); + }); + }); + gDebuggee.runTest(); + }); + }); +} + +function test_unblack_box_dbg_statement(aSourceClient) { + aSourceClient.unblackBox(function ({error}) { + do_check_true(!error, "Should not get an error: " + error); + + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "debuggerStatement", + "We should stop at the debugger statement again"); + finishClient(gClient); + }); + gDebuggee.runTest(); + }); +} diff --git a/devtools/server/tests/unit/test_blackboxing-04.js b/devtools/server/tests/unit/test_blackboxing-04.js new file mode 100644 index 000000000..fbfaf2881 --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-04.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test behavior of blackboxing sources we are currently paused in. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_black_box(); + }); + }); + do_test_pending(); +} + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function test_black_box() +{ + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.eval(aPacket.frame.actor, "doStuff", function (aResponse) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let obj = gThreadClient.pauseGrip(aPacket.why.frameFinished.return); + obj.getDefinitionSite(runWithSource); + }); + }); + + function runWithSource(aPacket) { + let source = gThreadClient.source(aPacket.source); + source.setBreakpoint({ + line: 2 + }, function (aResponse) { + do_check_true(!aResponse.error, "Should be able to set breakpoint."); + test_black_box_paused(); + }); + } + }); + + Components.utils.evalInSandbox( + "" + function doStuff(k) { // line 1 + debugger; // line 2 + k(100); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + Components.utils.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function (n) { // line 3 + return n; // line 4 + } // line 5 + ); // line 6 + } // line 7 + + "\n runTest();", // line 8 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +function test_black_box_paused() { + gThreadClient.getSources(function ({error, sources}) { + do_check_true(!error, "Should not get an error: " + error); + let sourceClient = gThreadClient.source(sources.filter(s => s.url == BLACK_BOXED_URL)[0]); + + sourceClient.blackBox(function ({error, pausedInSource}) { + do_check_true(!error, "Should not get an error: " + error); + do_check_true(pausedInSource, "We should be notified that we are currently paused in this source"); + finishClient(gClient); + }); + }); +} diff --git a/devtools/server/tests/unit/test_blackboxing-05.js b/devtools/server/tests/unit/test_blackboxing-05.js new file mode 100644 index 000000000..fa8142e87 --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-05.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test exceptions inside black boxed sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + // XXX: We have to do an executeSoon so that the error isn't caught and + // reported by DebuggerClient.requester (because we are using the local + // transport and share a stack) which causes the test to fail. + Services.tm.mainThread.dispatch({ + run: test_black_box + }, Ci.nsIThread.DISPATCH_NORMAL); + }); + }); + do_test_pending(); +} + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function test_black_box() +{ + gClient.addOneTimeListener("paused", test_black_box_exception); + + Components.utils.evalInSandbox( + "" + function doStuff(k) { // line 1 + throw new Error("wu tang clan ain't nuthin' ta fuck wit"); // line 2 + k(100); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + Components.utils.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function (n) { // line 3 + debugger; // line 4 + } // line 5 + ); // line 6 + } // line 7 + + "\ndebugger;\n" // line 8 + + "try { runTest() } catch (ex) { }", // line 9 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +function test_black_box_exception() { + gThreadClient.getSources(function ({error, sources}) { + do_check_true(!error, "Should not get an error: " + error); + let sourceClient = gThreadClient.source(sources.filter(s => s.url == BLACK_BOXED_URL)[0]); + + sourceClient.blackBox(function ({error}) { + do_check_true(!error, "Should not get an error: " + error); + gThreadClient.pauseOnExceptions(true); + + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.frame.where.source.url, SOURCE_URL, + "We shouldn't pause while in the black boxed source."); + finishClient(gClient); + }); + + gThreadClient.resume(); + }); + }); +} diff --git a/devtools/server/tests/unit/test_blackboxing-06.js b/devtools/server/tests/unit/test_blackboxing-06.js new file mode 100644 index 000000000..9384f2cc2 --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-06.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can black box source mapped sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + + promise.resolve(setup_code()) + .then(black_box_code) + .then(run_code) + .then(test_correct_location) + .then(null, function (error) { + do_check_true(false, "Should not get an error, got " + error); + }) + .then(function () { + finishClient(gClient); + }); + }); + }); + do_test_pending(); +} + +function setup_code() { + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "" + function a() { + return b(); + }), + "\n", + new SourceNode(1, 0, "b.js", "" + function b() { + debugger; // Don't want to stop here. + return c(); + }), + "\n", + new SourceNode(1, 0, "c.js", "" + function c() { + debugger; // Want to stop here. + }), + "\n" + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/" + }); + + code += "//# sourceMappingURL=data:text/json," + map.toString(); + + Components.utils.evalInSandbox(code, + gDebuggee, + "1.8", + "http://example.com/abc.js"); +} + +function black_box_code() { + const d = promise.defer(); + + gThreadClient.getSources(function ({ sources, error }) { + do_check_true(!error, "Shouldn't get an error getting sources"); + const source = sources.filter((s) => { + return s.url.indexOf("b.js") !== -1; + })[0]; + do_check_true(!!source, "We should have our source in the sources list"); + + gThreadClient.source(source).blackBox(function ({ error }) { + do_check_true(!error, "Should not get an error black boxing"); + d.resolve(true); + }); + }); + + return d.promise; +} + +function run_code() { + const d = promise.defer(); + + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + d.resolve(aPacket); + gThreadClient.resume(); + }); + gDebuggee.a(); + + return d.promise; +} + +function test_correct_location(aPacket) { + do_check_eq(aPacket.why.type, "debuggerStatement", + "Should hit a debugger statement."); + do_check_eq(aPacket.frame.where.source.url, "http://example.com/c.js", + "Should have skipped over the debugger statement in the black boxed source"); +} diff --git a/devtools/server/tests/unit/test_blackboxing-07.js b/devtools/server/tests/unit/test_blackboxing-07.js new file mode 100644 index 000000000..da3147021 --- /dev/null +++ b/devtools/server/tests/unit/test_blackboxing-07.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that sources whose URL ends with ".min.js" automatically get black + * boxed. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-black-box"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-black-box", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + testBlackBox(); + }); + }); + do_test_pending(); +} + +const BLACK_BOXED_URL = "http://example.com/black-boxed.min.js"; +const SOURCE_URL = "http://example.com/source.js"; + +const testBlackBox = Task.async(function* () { + yield executeOnNextTickAndWaitForPause(evalCode, gClient); + + const { sources } = yield getSources(gThreadClient); + equal(sources.length, 2); + + const blackBoxedSource = sources.filter(s => s.url === BLACK_BOXED_URL)[0]; + equal(blackBoxedSource.isBlackBoxed, true); + + const regularSource = sources.filter(s => s.url === SOURCE_URL)[0]; + equal(regularSource.isBlackBoxed, false); + + finishClient(gClient); +}); + +function evalCode() { + Components.utils.evalInSandbox( + "" + function blackBoxed() {}, + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + Components.utils.evalInSandbox( + "" + function source() {} + + "\ndebugger;", + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-01.js b/devtools/server/tests/unit/test_breakpoint-01.js new file mode 100644 index 000000000..9a20257a3 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-01.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic breakpoint functionality. + */ +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_breakpoint(); + }); + }); +} + +function test_simple_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { + line: gDebuggee.line0 + 3 + }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + }); + + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-02.js b/devtools/server/tests/unit/test_breakpoint-02.js new file mode 100644 index 000000000..d2b220d83 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-02.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting breakpoints when the debuggee is running works. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_breakpoint_running(); + }); + }); +} + +function test_breakpoint_running() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let location = { line: gDebuggee.line0 + 3 }; + + gThreadClient.resume(); + + // Setting the breakpoint later should interrupt the debuggee. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "interrupted"); + }); + + let source = gThreadClient.source(aPacket.frame.where.source); + source.setBreakpoint(location, function (aResponse) { + // Eval scripts don't stick around long enough for the breakpoint to be set, + // so just make sure we got the expected response from the actor. + do_check_neq(aResponse.error, "noScript"); + + do_execute_soon(function () { + gClient.close().then(gCallback); + }); + }); + }); + + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-03.js b/devtools/server/tests/unit/test_breakpoint-03.js new file mode 100644 index 000000000..b1792866b --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-03.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint on a line without code will skip + * forward when we know the script isn't GCed (the debugger is connected, + * so it's kept alive). + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, + "test-stack", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_skip_breakpoint(); + }); + }); +} + +var test_no_skip_breakpoint = Task.async(function*(source, location) { + let [aResponse, bpClient] = yield source.setBreakpoint( + Object.assign({}, location, { noSliding: true }) + ); + + do_check_true(!aResponse.actualLocation); + do_check_eq(bpClient.location.line, gDebuggee.line0 + 3); + yield bpClient.remove(); +}); + +var test_skip_breakpoint = function() { + gThreadClient.addOneTimeListener("paused", Task.async(function *(aEvent, aPacket) { + let location = { line: gDebuggee.line0 + 3 }; + let source = gThreadClient.source(aPacket.frame.where.source); + + // First, make sure that we can disable sliding with the + // `noSliding` option. + yield test_no_skip_breakpoint(source, location); + + // Now make sure that the breakpoint properly slides forward one line. + const [aResponse, bpClient] = yield source.setBreakpoint(location); + do_check_true(!!aResponse.actualLocation); + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line + 1); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + gThreadClient.resume(); + })); + + // Use `evalInSandbox` to make the debugger treat it as normal + // globally-scoped code, where breakpoint sliding rules apply. + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "// A comment.\n" + // line0 + 3 + "var b = 2;", // line0 + 4 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-04.js b/devtools/server/tests/unit/test_breakpoint-04.js new file mode 100644 index 000000000..9004c092b --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-04.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint in a line in a child script works. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_child_breakpoint(); + }); + }); +} + +function test_child_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 3 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // actualLocation is not returned when breakpoints don't skip forward. + do_check_eq(aResponse.actualLocation, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + + }); + + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " this.b = 2;\n" + // line0 + 3 + "}\n" + // line0 + 4 + "debugger;\n" + // line0 + 5 + "foo();\n", // line0 + 6 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-05.js b/devtools/server/tests/unit/test_breakpoint-05.js new file mode 100644 index 000000000..9e04d0271 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-05.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint in a line without code in a child script + * will skip forward. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_child_skip_breakpoint(); + }); + }); +} + +function test_child_skip_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 3 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // Check that the breakpoint has properly skipped forward one line. + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line + 1); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + }); + + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "foo();\n", // line0 + 7 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-06.js b/devtools/server/tests/unit/test_breakpoint-06.js new file mode 100644 index 000000000..aa92b1a5f --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-06.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint in a line without code in a deeply-nested + * child script will skip forward. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_nested_breakpoint(); + }); + }); +} + +function test_nested_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 5 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // Check that the breakpoint has properly skipped forward one line. + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line + 1); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + + }); + + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " function bar() {\n" + // line0 + 2 + " function baz() {\n" + // line0 + 3 + " this.a = 1;\n" + // line0 + 4 + " // A comment.\n" + // line0 + 5 + " this.b = 2;\n" + // line0 + 6 + " }\n" + // line0 + 7 + " baz();\n" + // line0 + 8 + " }\n" + // line0 + 9 + " bar();\n" + // line0 + 10 + "}\n" + // line0 + 11 + "debugger;\n" + // line0 + 12 + "foo();\n", // line0 + 13 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-07.js b/devtools/server/tests/unit/test_breakpoint-07.js new file mode 100644 index 000000000..008f1424d --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-07.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint in a line without code in the second child + * script will skip forward. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_second_child_skip_breakpoint(); + }); + }); +} + +function test_second_child_skip_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 6 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // Check that the breakpoint has properly skipped forward one line. + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line + 1); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + }); + + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " bar();\n" + // line0 + 2 + "}\n" + // line0 + 3 + "function bar() {\n" + // line0 + 4 + " this.a = 1;\n" + // line0 + 5 + " // A comment.\n" + // line0 + 6 + " this.b = 2;\n" + // line0 + 7 + "}\n" + // line0 + 8 + "debugger;\n" + // line0 + 9 + "foo();\n", // line0 + 10 + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-08.js b/devtools/server/tests/unit/test_breakpoint-08.js new file mode 100644 index 000000000..6215a61ac --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-08.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint in a line without code in a child script + * will skip forward, in a file with two scripts. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_child_skip_breakpoint(); + }); + }); +} + +function test_child_skip_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.eval(aPacket.frame.actor, "foo", function (aResponse) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let obj = gThreadClient.pauseGrip(aPacket.why.frameFinished.return); + obj.getDefinitionSite(runWithBreakpoint); + }); + }); + + function runWithBreakpoint(aPacket) { + let source = gThreadClient.source(aPacket.source); + let location = { line: gDebuggee.line0 + 3 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // Check that the breakpoint has properly skipped forward one line. + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line + 1); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + } + }); + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n", // line0 + 5 + gDebuggee, + "1.7", + "script1.js"); + + Cu.evalInSandbox("var line1 = Error().lineNumber;\n" + + "debugger;\n" + // line1 + 1 + "foo();\n", // line1 + 2 + gDebuggee, + "1.7", + "script2.js"); +} diff --git a/devtools/server/tests/unit/test_breakpoint-09.js b/devtools/server/tests/unit/test_breakpoint-09.js new file mode 100644 index 000000000..8bea375b9 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-09.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that removing a breakpoint works. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_remove_breakpoint(); + }); + }); +} + +function test_remove_breakpoint() +{ + let done = false; + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 2 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + done = true; + gThreadClient.addOneTimeListener("paused", + function (aEvent, aPacket) { + // The breakpoint should not be hit again. + gThreadClient.resume(function () { + do_check_true(false); + }); + }); + gThreadClient.resume(); + }); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + + }); + + }); + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo(stop) {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " if (stop) return;\n" + // line0 + 3 + " delete this.a;\n" + // line0 + 4 + " foo(true);\n" + // line0 + 5 + "}\n" + // line0 + 6 + "debugger;\n" + // line1 + 7 + "foo();\n", // line1 + 8 + gDebuggee); + if (!done) { + do_check_true(false); + } + gClient.close().then(gCallback); +} diff --git a/devtools/server/tests/unit/test_breakpoint-10.js b/devtools/server/tests/unit/test_breakpoint-10.js new file mode 100644 index 000000000..c69576767 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-10.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that setting a breakpoint in a line with multiple entry points + * triggers no matter which entry point we reach. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_child_breakpoint(); + }); + }); +} + +function test_child_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 3 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // actualLocation is not returned when breakpoints don't skip forward. + do_check_eq(aResponse.actualLocation, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.i, 0); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.i, 1); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit again. + gThreadClient.resume(); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + + }); + + }); + + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a, i = 0;\n" + // line0 + 2 + "for (i = 1; i <= 2; i++) {\n" + // line0 + 3 + " a = i;\n" + // line0 + 4 + "}\n", // line0 + 5 + gDebuggee); +} diff --git a/devtools/server/tests/unit/test_breakpoint-11.js b/devtools/server/tests/unit/test_breakpoint-11.js new file mode 100644 index 000000000..480b95984 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-11.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that setting a breakpoint in a line with bytecodes in multiple + * scripts, sets the breakpoint in all of them (bug 793214). + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_child_breakpoint(); + }); + }); +} + +function test_child_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 2 }; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // actualLocation is not returned when breakpoints don't skip forward. + do_check_eq(aResponse.actualLocation, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a.b, 1); + do_check_eq(gDebuggee.res, undefined); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + // Continue until the breakpoint is hit again. + gThreadClient.resume(); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + + }); + + }); + + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = { b: 1, f: function() { return 2; } };\n" + // line0+2 + "var res = a.f();\n", // line0 + 3 + gDebuggee); +} diff --git a/devtools/server/tests/unit/test_breakpoint-12.js b/devtools/server/tests/unit/test_breakpoint-12.js new file mode 100644 index 000000000..fb147da9f --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-12.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that setting a breakpoint twice in a line without bytecodes works + * as expected. + */ + +const NUM_BREAKPOINTS = 10; +var gDebuggee; +var gClient; +var gThreadClient; +var gBpActor; +var gCount; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + gCount = 1; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_child_skip_breakpoint(); + }); + }); +} + +function test_child_skip_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 3}; + + source.setBreakpoint(location, function (aResponse, bpClient) { + // Check that the breakpoint has properly skipped forward one line. + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + gBpActor = aResponse.actor; + + // Set more breakpoints at the same location. + set_breakpoints(source, location); + }); + + }); + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "foo();\n", // line0 + 7 + gDebuggee); +} + +// Set many breakpoints at the same location. +function set_breakpoints(source, location) { + do_check_neq(gCount, NUM_BREAKPOINTS); + source.setBreakpoint(location, function (aResponse, bpClient) { + // Check that the breakpoint has properly skipped forward one line. + do_check_eq(aResponse.actualLocation.source.actor, source.actor); + do_check_eq(aResponse.actualLocation.line, location.line + 1); + // Check that the same breakpoint actor was returned. + do_check_eq(aResponse.actor, gBpActor); + + if (++gCount < NUM_BREAKPOINTS) { + set_breakpoints(source, location); + return; + } + + // After setting all the breakpoints, check that only one has effectively + // remained. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line + 1); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // We don't expect any more pauses after the breakpoint was hit once. + do_check_true(false); + }); + gThreadClient.resume(function () { + // Give any remaining breakpoints a chance to trigger. + do_timeout(1000, function () { + gClient.close().then(gCallback); + }); + }); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + +} diff --git a/devtools/server/tests/unit/test_breakpoint-13.js b/devtools/server/tests/unit/test_breakpoint-13.js new file mode 100644 index 000000000..cdc4c9091 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-13.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that execution doesn't pause twice while stepping, when encountering + * either a breakpoint or a debugger statement. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_breakpoint(); + }); + }); +} + +function test_simple_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 2 }; + + source.setBreakpoint(location, Task.async(function* (aResponse, bpClient) { + const testCallbacks = [ + function (aPacket) { + // Check that the stepping worked. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // Entered the foo function call frame. + do_check_eq(aPacket.frame.where.line, location.line); + do_check_neq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // At the end of the foo function call frame. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_neq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // Check that the breakpoint wasn't the reason for this pause, but + // that the frame is about to be popped while stepping. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_neq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.why.frameFinished.return.type, "undefined"); + }, + function (aPacket) { + // The foo function call frame was just popped from the stack. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.poppedFrames.length, 1); + }, + function (aPacket) { + // Check that the debugger statement wasn't the reason for this pause. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6); + do_check_neq(aPacket.why.type, "debuggerStatement"); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // Check that the debugger statement wasn't the reason for this pause. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 7); + do_check_neq(aPacket.why.type, "debuggerStatement"); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + ]; + + for (let callback of testCallbacks) { + let waiter = waitForPause(gThreadClient); + gThreadClient.stepIn(); + let packet = yield waiter; + callback(packet); + } + + // Remove the breakpoint and finish. + let waiter = waitForPause(gThreadClient); + gThreadClient.stepIn(); + yield waiter; + bpClient.remove(() => gThreadClient.resume(() => gClient.close().then(gCallback))); + })); + }); + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 <-- Breakpoint is set here. + "}\n" + // line0 + 3 + "debugger;\n" + // line0 + 4 + "foo();\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "var b = 2;\n", // line0 + 7 + gDebuggee); +} diff --git a/devtools/server/tests/unit/test_breakpoint-14.js b/devtools/server/tests/unit/test_breakpoint-14.js new file mode 100644 index 000000000..aa86975b6 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-14.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that a breakpoint or a debugger statement cause execution to pause even + * in a stepped-over function. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_breakpoint(); + }); + }); +} + +function test_simple_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { line: gDebuggee.line0 + 2 }; + + source.setBreakpoint(location, Task.async(function* (aResponse, bpClient) { + const testCallbacks = [ + function (aPacket) { + // Check that the stepping worked. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // Reached the breakpoint. + do_check_eq(aPacket.frame.where.line, location.line); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_neq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // Stepped to the closing brace of the function. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // The frame is about to be popped while stepping. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_neq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.why.frameFinished.return.type, "undefined"); + }, + function (aPacket) { + // The foo function call frame was just popped from the stack. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.poppedFrames.length, 1); + }, + function (aPacket) { + // Check that the debugger statement wasn't the reason for this pause. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6); + do_check_neq(aPacket.why.type, "debuggerStatement"); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + function (aPacket) { + // Check that the debugger statement wasn't the reason for this pause. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 7); + do_check_neq(aPacket.why.type, "debuggerStatement"); + do_check_eq(aPacket.why.type, "resumeLimit"); + }, + ]; + + for (let callback of testCallbacks) { + let waiter = waitForPause(gThreadClient); + gThreadClient.stepOver(); + let packet = yield waiter; + callback(packet); + } + + // Remove the breakpoint and finish. + let waiter = waitForPause(gThreadClient); + gThreadClient.stepOver(); + yield waiter; + bpClient.remove(() => gThreadClient.resume(() => gClient.close().then(gCallback))); + })); + }); + + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 <-- Breakpoint is set here. + "}\n" + // line0 + 3 + "debugger;\n" + // line0 + 4 + "foo();\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "var b = 2;\n", // line0 + 7 + gDebuggee); +} diff --git a/devtools/server/tests/unit/test_breakpoint-15.js b/devtools/server/tests/unit/test_breakpoint-15.js new file mode 100644 index 000000000..6a3ab7c6f --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-15.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that adding a breakpoint in the same place returns the same actor. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + testSameBreakpoint(); + }); + }); + do_test_pending(); +} + +const SOURCE_URL = "http://example.com/source.js"; + +const testSameBreakpoint = Task.async(function* () { + let packet = yield executeOnNextTickAndWaitForPause(evalCode, gClient); + let source = gThreadClient.source(packet.frame.where.source); + + // Whole line + let wholeLineLocation = { + line: 2 + }; + + let [firstResponse, firstBpClient] = yield setBreakpoint(source, wholeLineLocation); + let [secondResponse, secondBpClient] = yield setBreakpoint(source, wholeLineLocation); + + do_check_eq(firstBpClient.actor, secondBpClient.actor, "Should get the same actor w/ whole line breakpoints"); + + // Specific column + + let columnLocation = { + line: 2, + column: 6 + }; + + [firstResponse, firstBpClient] = yield setBreakpoint(source, columnLocation); + [secondResponse, secondBpClient] = yield setBreakpoint(source, columnLocation); + + do_check_eq(secondBpClient.actor, secondBpClient.actor, "Should get the same actor column breakpoints"); + + finishClient(gClient); +}); + +function evalCode() { + Components.utils.evalInSandbox( + "" + function doStuff(k) { // line 1 + let arg = 15; // line 2 - Step in here + k(arg); // line 3 + } + "\n" // line 4 + + "debugger;", // line 5 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-16.js b/devtools/server/tests/unit/test_breakpoint-16.js new file mode 100644 index 000000000..43a9086ec --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-16.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we can set breakpoints in columns, not just lines. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-breakpoints", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, + "test-breakpoints", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_column_breakpoint(); + }); + }); +} + +function test_column_breakpoint() +{ + // Debugger statement + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + let location = { + line: gDebuggee.line0 + 1, + column: 55 + }; + let timesBreakpointHit = 0; + + source.setBreakpoint(location, function (aResponse, bpClient) { + gThreadClient.addListener("paused", function onPaused(aEvent, aPacket) { + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.why.actors[0], bpClient.actor); + do_check_eq(aPacket.frame.where.source.actor, source.actor); + do_check_eq(aPacket.frame.where.line, location.line); + do_check_eq(aPacket.frame.where.column, location.column); + + do_check_eq(gDebuggee.acc, timesBreakpointHit); + do_check_eq(aPacket.frame.environment.bindings.variables.i.value, + timesBreakpointHit); + + if (++timesBreakpointHit === 3) { + gThreadClient.removeListener("paused", onPaused); + bpClient.remove(function (aResponse) { + gThreadClient.resume(() => gClient.close().then(gCallback)); + }); + } else { + gThreadClient.resume(); + } + }); + + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + + }); + + + Components.utils.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "(function () { debugger; this.acc = 0; for (var i = 0; i < 3; i++) this.acc++; }());", + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-17.js b/devtools/server/tests/unit/test_breakpoint-17.js new file mode 100644 index 000000000..944627e61 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-17.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that when we add 2 breakpoints to the same line at different columns and + * then remove one of them, we don't remove them both. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, do_test_finished); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-breakpoints", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-breakpoints", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_breakpoints_columns(); + }); + }); +} + +const code = +"(" + function (global) { + global.foo = function () { + Math.abs(-1); Math.log(0.5); + debugger; + }; + debugger; +} + "(this))"; + +const firstLocation = { + line: 3, + column: 4 +}; + +const secondLocation = { + line: 3, + column: 18 +}; + +function test_breakpoints_columns() { + gClient.addOneTimeListener("paused", set_breakpoints); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", "http://example.com/", 1); +} + +function set_breakpoints(aEvent, aPacket) { + let first, second; + let source = gThreadClient.source(aPacket.frame.where.source); + + source.setBreakpoint(firstLocation, function ({ error, actualLocation }, + aBreakpointClient) { + do_check_true(!error, "Should not get an error setting the breakpoint"); + do_check_true(!actualLocation, "Should not get an actualLocation"); + first = aBreakpointClient; + + source.setBreakpoint(secondLocation, function ({ error, actualLocation }, + aBreakpointClient) { + do_check_true(!error, "Should not get an error setting the breakpoint"); + do_check_true(!actualLocation, "Should not get an actualLocation"); + second = aBreakpointClient; + + test_different_actors(first, second); + }); + }); +} + +function test_different_actors(aFirst, aSecond) { + do_check_neq(aFirst.actor, aSecond.actor, + "Each breakpoint should have a different actor"); + test_remove_one(aFirst, aSecond); +} + +function test_remove_one(aFirst, aSecond) { + aFirst.remove(function ({error}) { + do_check_true(!error, "Should not get an error removing a breakpoint"); + + let hitSecond; + gClient.addListener("paused", function _onPaused(aEvent, {why, frame}) { + if (why.type == "breakpoint") { + hitSecond = true; + do_check_eq(why.actors.length, 1, + "Should only be paused because of one breakpoint actor"); + do_check_eq(why.actors[0], aSecond.actor, + "Should be paused because of the correct breakpoint actor"); + do_check_eq(frame.where.line, secondLocation.line, + "Should be at the right line"); + do_check_eq(frame.where.column, secondLocation.column, + "Should be at the right column"); + gThreadClient.resume(); + return; + } + + if (why.type == "debuggerStatement") { + gClient.removeListener("paused", _onPaused); + do_check_true(hitSecond, + "We should still hit `second`, but not `first`."); + + gClient.close().then(gCallback); + return; + } + + do_check_true(false, "Should never get here"); + }); + + gThreadClient.resume(() => gDebuggee.foo()); + }); +} diff --git a/devtools/server/tests/unit/test_breakpoint-18.js b/devtools/server/tests/unit/test_breakpoint-18.js new file mode 100644 index 000000000..d153d3eff --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-18.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we only break on offsets that are entry points for the line we are + * breaking on. Bug 907278. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-breakpoints", aServer); + gDebuggee.console = { log: x => void x }; + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, + "test-breakpoints", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + setUpCode(); + }); + }); +} + +function setUpCode() { + gClient.addOneTimeListener("paused", setBreakpoint); + Cu.evalInSandbox( + "debugger;\n" + + function test() { + console.log("foo bar"); + debugger; + }, + gDebuggee, + "1.8", + "http://example.com/", + 1 + ); +} + +function setBreakpoint(aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + gClient.addOneTimeListener("resumed", runCode); + + source.setBreakpoint({ line: 2 }, ({ error }) => { + do_check_true(!error); + gThreadClient.resume(); + }); +} + +function runCode() { + gClient.addOneTimeListener("paused", testBPHit); + gDebuggee.test(); +} + +function testBPHit(event, { why }) { + do_check_eq(why.type, "breakpoint"); + gClient.addOneTimeListener("paused", testDbgStatement); + gThreadClient.resume(); +} + +function testDbgStatement(event, { why }) { + // Should continue to the debugger statement. + do_check_eq(why.type, "debuggerStatement"); + // Not break on another offset from the same line (that isn't an entry point + // to the line) + do_check_neq(why.type, "breakpoint"); + gClient.close().then(gCallback); +} diff --git a/devtools/server/tests/unit/test_breakpoint-19.js b/devtools/server/tests/unit/test_breakpoint-19.js new file mode 100644 index 000000000..da04a5268 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-19.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that setting a breakpoint in a not-yet-existing script doesn't throw + * an error (see bug 897567). Also make sure that this breakpoint works. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-breakpoints", aServer); + gDebuggee.console = { log: x => void x }; + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, + "test-breakpoints", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + testBreakpoint(); + }); + }); +} + +const URL = "test.js"; + +function setUpCode() { + Cu.evalInSandbox( + "" + function test() { // 1 + var a = 1; // 2 + debugger; // 3 + } + // 4 + "\ndebugger;", // 5 + gDebuggee, + "1.8", + URL + ); +} + +const testBreakpoint = Task.async(function* () { + let source = yield getSource(gThreadClient, URL); + let [response, bpClient] = yield setBreakpoint(source, {line: 2}); + ok(!response.error); + + let actor = response.actor; + ok(actor); + + yield executeOnNextTickAndWaitForPause(setUpCode, gClient); + yield resume(gThreadClient); + + let packet = yield executeOnNextTickAndWaitForPause(gDebuggee.test, gClient); + equal(packet.why.type, "breakpoint"); + notEqual(packet.why.actors.indexOf(actor), -1); + + finishClient(gClient); +}); diff --git a/devtools/server/tests/unit/test_breakpoint-20.js b/devtools/server/tests/unit/test_breakpoint-20.js new file mode 100644 index 000000000..b70282dae --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-20.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that when two of the "same" source are loaded concurrently (like e10s + * frame scripts), breakpoints get hit in scripts defined by all sources. + */ + +var gDebuggee; +var gClient; +var gTraceClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-breakpoints"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestThread(gClient, "test-breakpoints", testBreakpoint); + }); + do_test_pending(); +} + +const testBreakpoint = Task.async(function* (threadResponse, tabClient, threadClient, tabResponse) { + evalSetupCode(); + + // Load the test source once. + + evalTestCode(); + equal(gDebuggee.functions.length, 1, + "The test code should have added a function."); + + // Set a breakpoint in the test source. + + const source = yield getSource(threadClient, "test.js"); + const [response, bpClient] = yield setBreakpoint(source, { + line: 3 + }); + ok(!response.error, "Shouldn't get an error setting the BP."); + ok(!response.actualLocation, + "Shouldn't get an actualLocation, the location we provided was good."); + const bpActor = response.actor; + + yield resume(threadClient); + + // Load the test source again. + + evalTestCode(); + equal(gDebuggee.functions.length, 2, + "The test code should have added another function."); + + // Should hit our breakpoint in a script defined by the first instance of the + // test source. + + const bpPause1 = yield executeOnNextTickAndWaitForPause(gDebuggee.functions[0], + gClient); + equal(bpPause1.why.type, "breakpoint", + "Should pause because of hitting our breakpoint (not debugger statement)."); + equal(bpPause1.why.actors[0], bpActor, + "And the breakpoint actor should be correct."); + const dbgStmtPause1 = yield executeOnNextTickAndWaitForPause(() => resume(threadClient), + gClient); + equal(dbgStmtPause1.why.type, "debuggerStatement", + "And we should hit the debugger statement after the pause."); + yield resume(threadClient); + + // Should also hit our breakpoint in a script defined by the second instance + // of the test source. + + const bpPause2 = yield executeOnNextTickAndWaitForPause(gDebuggee.functions[1], + gClient); + equal(bpPause2.why.type, "breakpoint", + "Should pause because of hitting our breakpoint (not debugger statement)."); + equal(bpPause2.why.actors[0], bpActor, + "And the breakpoint actor should be correct."); + const dbgStmtPause2 = yield executeOnNextTickAndWaitForPause(() => resume(threadClient), + gClient); + equal(dbgStmtPause2.why.type, "debuggerStatement", + "And we should hit the debugger statement after the pause."); + + finishClient(gClient); +}); + +function evalSetupCode() { + Cu.evalInSandbox( + "this.functions = [];", + gDebuggee, + "1.8", + "setup.js", + 1 + ); +} + +function evalTestCode() { + Cu.evalInSandbox( + ` // 1 + this.functions.push(function () { // 2 + var setBreakpointHere = 1; // 3 + debugger; // 4 + }); // 5 + `, + gDebuggee, + "1.8", + "test.js", + 1 + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-21.js b/devtools/server/tests/unit/test_breakpoint-21.js new file mode 100644 index 000000000..e5f2e9e4a --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-21.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 1122064 - make sure that scripts introduced via onNewScripts + * properly populate the `ScriptStore` with all there nested child + * scripts, so you can set breakpoints on deeply nested scripts + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-breakpoints", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, + "test-breakpoints", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test(); + }); + }); +} + +const test = Task.async(function* () { + // Populate the `ScriptStore` so that we only test that the script + // is added through `onNewScript` + yield getSources(gThreadClient); + + let packet = yield executeOnNextTickAndWaitForPause(evalCode, gClient); + let source = gThreadClient.source(packet.frame.where.source); + let location = { + line: gDebuggee.line0 + 8 + }; + + let [res, bpClient] = yield setBreakpoint(source, location); + ok(!res.error); + + yield resume(gThreadClient); + packet = yield waitForPause(gClient); + do_check_eq(packet.type, "paused"); + do_check_eq(packet.why.type, "breakpoint"); + do_check_eq(packet.why.actors[0], bpClient.actor); + do_check_eq(packet.frame.where.source.actor, source.actor); + do_check_eq(packet.frame.where.line, location.line); + + yield resume(gThreadClient); + finishClient(gClient); +}); + +function evalCode() { + // Start a new script + Components.utils.evalInSandbox( + "var line0 = Error().lineNumber;\n(" + function () { + debugger; + var a = (function () { + return (function () { + return (function () { + return (function () { + return (function () { + var x = 10; // This line gets a breakpoint + return 1; + })(); + })(); + })(); + })(); + })(); + } + ")()", + gDebuggee + ); +} diff --git a/devtools/server/tests/unit/test_breakpoint-actor-map.js b/devtools/server/tests/unit/test_breakpoint-actor-map.js new file mode 100644 index 000000000..d1d149648 --- /dev/null +++ b/devtools/server/tests/unit/test_breakpoint-actor-map.js @@ -0,0 +1,180 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the functionality of the BreakpointActorMap object. + +const { BreakpointActorMap } = require("devtools/server/actors/script"); + +function run_test() { + test_get_actor(); + test_set_actor(); + test_delete_actor(); + test_find_actors(); + test_duplicate_actors(); +} + +function test_get_actor() { + let bpStore = new BreakpointActorMap(); + let location = { + originalSourceActor: { actor: "actor1" }, + originalLine: 3 + }; + let columnLocation = { + originalSourceActor: { actor: "actor2" }, + originalLine: 5, + originalColumn: 15 + }; + + // Shouldn't have breakpoint + do_check_eq(null, bpStore.getActor(location), + "Breakpoint not added and shouldn't exist."); + + bpStore.setActor(location, {}); + do_check_true(!!bpStore.getActor(location), + "Breakpoint added but not found in Breakpoint Store."); + + bpStore.deleteActor(location); + do_check_eq(null, bpStore.getActor(location), + "Breakpoint removed but still exists."); + + // Same checks for breakpoint with a column + do_check_eq(null, bpStore.getActor(columnLocation), + "Breakpoint with column not added and shouldn't exist."); + + bpStore.setActor(columnLocation, {}); + do_check_true(!!bpStore.getActor(columnLocation), + "Breakpoint with column added but not found in Breakpoint Store."); + + bpStore.deleteActor(columnLocation); + do_check_eq(null, bpStore.getActor(columnLocation), + "Breakpoint with column removed but still exists in Breakpoint Store."); +} + +function test_set_actor() { + // Breakpoint with column + let bpStore = new BreakpointActorMap(); + let location = { + originalSourceActor: { actor: "actor1" }, + originalLine: 10, + originalColumn: 9 + }; + bpStore.setActor(location, {}); + do_check_true(!!bpStore.getActor(location), + "We should have the column breakpoint we just added"); + + // Breakpoint without column (whole line breakpoint) + location = { + originalSourceActor: { actor: "actor2" }, + originalLine: 103 + }; + bpStore.setActor(location, {}); + do_check_true(!!bpStore.getActor(location), + "We should have the whole line breakpoint we just added"); +} + +function test_delete_actor() { + // Breakpoint with column + let bpStore = new BreakpointActorMap(); + let location = { + originalSourceActor: { actor: "actor1" }, + originalLine: 10, + originalColumn: 9 + }; + bpStore.setActor(location, {}); + bpStore.deleteActor(location); + do_check_eq(bpStore.getActor(location), null, + "We should not have the column breakpoint anymore"); + + // Breakpoint without column (whole line breakpoint) + location = { + originalSourceActor: { actor: "actor2" }, + originalLine: 103 + }; + bpStore.setActor(location, {}); + bpStore.deleteActor(location); + do_check_eq(bpStore.getActor(location), null, + "We should not have the whole line breakpoint anymore"); +} + +function test_find_actors() { + let bps = [ + { originalSourceActor: { actor: "actor1" }, originalLine: 10 }, + { originalSourceActor: { actor: "actor1" }, originalLine: 10, originalColumn: 3 }, + { originalSourceActor: { actor: "actor1" }, originalLine: 10, originalColumn: 10 }, + { originalSourceActor: { actor: "actor1" }, originalLine: 23, originalColumn: 89 }, + { originalSourceActor: { actor: "actor2" }, originalLine: 10, originalColumn: 1 }, + { originalSourceActor: { actor: "actor2" }, originalLine: 20, originalColumn: 5 }, + { originalSourceActor: { actor: "actor2" }, originalLine: 30, originalColumn: 34 }, + { originalSourceActor: { actor: "actor2" }, originalLine: 40, originalColumn: 56 } + ]; + + let bpStore = new BreakpointActorMap(); + + for (let bp of bps) { + bpStore.setActor(bp, bp); + } + + // All breakpoints + + let bpSet = new Set(bps); + for (let bp of bpStore.findActors()) { + bpSet.delete(bp); + } + do_check_eq(bpSet.size, 0, + "Should be able to iterate over all breakpoints"); + + // Breakpoints by URL + + bpSet = new Set(bps.filter(bp => { return bp.originalSourceActor.actorID === "actor1"; })); + for (let bp of bpStore.findActors({ originalSourceActor: { actorID: "actor1" } })) { + bpSet.delete(bp); + } + do_check_eq(bpSet.size, 0, + "Should be able to filter the iteration by url"); + + // Breakpoints by URL and line + + bpSet = new Set(bps.filter(bp => { return bp.originalSourceActor.actorID === "actor1" && bp.originalLine === 10; })); + let first = true; + for (let bp of bpStore.findActors({ originalSourceActor: { actorID: "actor1" }, originalLine: 10 })) { + if (first) { + do_check_eq(bp.originalColumn, undefined, + "Should always get the whole line breakpoint first"); + first = false; + } else { + do_check_neq(bp.originalColumn, undefined, + "Should not get the whole line breakpoint any time other than first."); + } + bpSet.delete(bp); + } + do_check_eq(bpSet.size, 0, + "Should be able to filter the iteration by url and line"); +} + +function test_duplicate_actors() { + let bpStore = new BreakpointActorMap(); + + // Breakpoint with column + let location = { + originalSourceActor: { actorID: "foo-actor" }, + originalLine: 10, + originalColumn: 9 + }; + bpStore.setActor(location, {}); + bpStore.setActor(location, {}); + do_check_eq(bpStore.size, 1, "We should have only 1 column breakpoint"); + bpStore.deleteActor(location); + + // Breakpoint without column (whole line breakpoint) + location = { + originalSourceActor: { actorID: "foo-actor" }, + originalLine: 15 + }; + bpStore.setActor(location, {}); + bpStore.setActor(location, {}); + do_check_eq(bpStore.size, 1, "We should have only 1 whole line breakpoint"); + bpStore.deleteActor(location); +} diff --git a/devtools/server/tests/unit/test_client_close.js b/devtools/server/tests/unit/test_client_close.js new file mode 100644 index 000000000..84747e85b --- /dev/null +++ b/devtools/server/tests/unit/test_client_close.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gClient; +var gDebuggee; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = testGlobal("test-1"); + DebuggerServer.addTestGlobal(gDebuggee); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect().then(function (aType, aTraits) { + attachTestTab(gClient, "test-1", function (aReply, aTabClient) { + test_close(transport); + }); + }); + do_test_pending(); +} + +function test_close(aTransport) +{ + // Check that, if we fake a transport shutdown + // (like if a device is unplugged) + // the client is automatically closed, + // and we can still call client.close. + let onClosed = function () { + gClient.removeListener("closed", onClosed); + ok(true, "Client emitted 'closed' event"); + gClient.close().then(function () { + ok(true, "client.close() successfully called its callback"); + do_test_finished(); + }); + }; + gClient.addListener("closed", onClosed); + aTransport.close(); +} diff --git a/devtools/server/tests/unit/test_client_request.js b/devtools/server/tests/unit/test_client_request.js new file mode 100644 index 000000000..c0c2c3a92 --- /dev/null +++ b/devtools/server/tests/unit/test_client_request.js @@ -0,0 +1,214 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the DebuggerClient.request API. + +var gClient, gActorId; + +function TestActor(conn) { + this.conn = conn; +} +TestActor.prototype = { + actorPrefix: "test", + + hello: function () { + return {hello: "world"}; + }, + + error: function () { + return {error: "code", message: "human message"}; + } +}; +TestActor.prototype.requestTypes = { + "hello": TestActor.prototype.hello, + "error": TestActor.prototype.error +}; + +function run_test() +{ + DebuggerServer.addGlobalActor(TestActor); + + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + add_test(init); + add_test(test_client_request_callback); + add_test(test_client_request_promise); + add_test(test_client_request_promise_error); + add_test(test_client_request_event_emitter); + add_test(test_close_client_while_sending_requests); + add_test(test_client_request_after_close); + add_test(test_client_request_after_close_callback); + run_next_test(); +} + +function init() +{ + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect() + .then(() => gClient.listTabs()) + .then(aResponse => { + gActorId = aResponse.test; + run_next_test(); + }); +} + +function checkStack(expectedName) { + if (!Services.prefs.getBoolPref("javascript.options.asyncstack")) { + do_print("Async stacks are disabled."); + return; + } + + let stack = Components.stack; + while (stack) { + do_print(stack.name); + if (stack.name == expectedName) { + // Reached back to outer function before request + ok(true, "Complete stack"); + return; + } + stack = stack.asyncCaller || stack.caller; + } + ok(false, "Incomplete stack"); +} + +function test_client_request_callback() +{ + // Test that DebuggerClient.request accepts a `onResponse` callback as 2nd argument + gClient.request({ + to: gActorId, + type: "hello" + }, response => { + do_check_eq(response.from, gActorId); + do_check_eq(response.hello, "world"); + checkStack("test_client_request_callback"); + run_next_test(); + }); +} + +function test_client_request_promise() +{ + // Test that DebuggerClient.request returns a promise that resolves on response + let request = gClient.request({ + to: gActorId, + type: "hello" + }); + + request.then(response => { + do_check_eq(response.from, gActorId); + do_check_eq(response.hello, "world"); + checkStack("test_client_request_promise"); + run_next_test(); + }); +} + +function test_client_request_promise_error() +{ + // Test that DebuggerClient.request returns a promise that reject when server + // returns an explicit error message + let request = gClient.request({ + to: gActorId, + type: "error" + }); + + request.then(() => { + do_throw("Promise shouldn't be resolved on error"); + }, response => { + do_check_eq(response.from, gActorId); + do_check_eq(response.error, "code"); + do_check_eq(response.message, "human message"); + checkStack("test_client_request_promise_error"); + run_next_test(); + }); +} + +function test_client_request_event_emitter() +{ + // Test that DebuggerClient.request returns also an EventEmitter object + let request = gClient.request({ + to: gActorId, + type: "hello" + }); + request.on("json-reply", reply => { + do_check_eq(reply.from, gActorId); + do_check_eq(reply.hello, "world"); + checkStack("test_client_request_event_emitter"); + run_next_test(); + }); +} + +function test_close_client_while_sending_requests() { + // First send a first request that will be "active" + // while the connection is closed. + // i.e. will be sent but no response received yet. + let activeRequest = gClient.request({ + to: gActorId, + type: "hello" + }); + + // Pile up a second one that will be "pending". + // i.e. won't event be sent. + let pendingRequest = gClient.request({ + to: gActorId, + type: "hello" + }); + + let expectReply = promise.defer(); + gClient.expectReply("root", function (response) { + do_check_eq(response.error, "connectionClosed"); + do_check_eq(response.message, "server side packet can't be received as the connection just closed."); + expectReply.resolve(); + }); + + gClient.close().then(() => { + activeRequest.then(() => { + ok(false, "First request unexpectedly succeed while closing the connection"); + }, response => { + do_check_eq(response.error, "connectionClosed"); + do_check_eq(response.message, "'hello' active request packet to '" + gActorId + "' can't be sent as the connection just closed."); + }) + .then(() => pendingRequest) + .then(() => { + ok(false, "Second request unexpectedly succeed while closing the connection"); + }, response => { + do_check_eq(response.error, "connectionClosed"); + do_check_eq(response.message, "'hello' pending request packet to '" + gActorId + "' can't be sent as the connection just closed."); + }) + .then(() => expectReply.promise) + .then(run_next_test); + }); +} + +function test_client_request_after_close() +{ + // Test that DebuggerClient.request fails after we called client.close() + // (with promise API) + let request = gClient.request({ + to: gActorId, + type: "hello" + }); + + request.then(response => { + ok(false, "Request succeed even after client.close"); + }, response => { + ok(true, "Request failed after client.close"); + do_check_eq(response.error, "connectionClosed"); + ok(response.message.match(/'hello' request packet to '.*' can't be sent as the connection is closed./)); + run_next_test(); + }); +} + +function test_client_request_after_close_callback() +{ + // Test that DebuggerClient.request fails after we called client.close() + // (with callback API) + let request = gClient.request({ + to: gActorId, + type: "hello" + }, response => { + ok(true, "Request failed after client.close"); + do_check_eq(response.error, "connectionClosed"); + ok(response.message.match(/'hello' request packet to '.*' can't be sent as the connection is closed./)); + run_next_test(); + }); +} diff --git a/devtools/server/tests/unit/test_conditional_breakpoint-01.js b/devtools/server/tests/unit/test_conditional_breakpoint-01.js new file mode 100644 index 000000000..4661bb0c4 --- /dev/null +++ b/devtools/server/tests/unit/test_conditional_breakpoint-01.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check conditional breakpoint when condition evaluates to true. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-conditional-breakpoint"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-conditional-breakpoint", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_breakpoint(); + }); + }); + do_test_pending(); +} + +function test_simple_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + source.setBreakpoint({ + line: 3, + condition: "a === 1" + }, function (aResponse, bpClient) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.why.type, "breakpoint"); + do_check_eq(aPacket.frame.where.line, 3); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + + }); + + }); + + Components.utils.evalInSandbox("debugger;\n" + // 1 + "var a = 1;\n" + // 2 + "var b = 2;\n", // 3 + gDebuggee, + "1.8", + "test.js", + 1); +} diff --git a/devtools/server/tests/unit/test_conditional_breakpoint-02.js b/devtools/server/tests/unit/test_conditional_breakpoint-02.js new file mode 100644 index 000000000..873f76159 --- /dev/null +++ b/devtools/server/tests/unit/test_conditional_breakpoint-02.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check conditional breakpoint when condition evaluates to false. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-conditional-breakpoint"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-conditional-breakpoint", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_breakpoint(); + }); + }); + do_test_pending(); +} + +function test_simple_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + source.setBreakpoint({ + line: 3, + condition: "a === 2" + }, function (aResponse, bpClient) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.why.type, "debuggerStatement"); + do_check_eq(aPacket.frame.where.line, 4); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + }); + }); + + Components.utils.evalInSandbox("debugger;\n" + // 1 + "var a = 1;\n" + // 2 + "var b = 2;\n" + // 3 + "debugger;", // 4 + gDebuggee, + "1.8", + "test.js", + 1); +} diff --git a/devtools/server/tests/unit/test_conditional_breakpoint-03.js b/devtools/server/tests/unit/test_conditional_breakpoint-03.js new file mode 100644 index 000000000..d9cf13e00 --- /dev/null +++ b/devtools/server/tests/unit/test_conditional_breakpoint-03.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check conditional breakpoint when condition throws and make sure it pauses + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-conditional-breakpoint"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-conditional-breakpoint", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_breakpoint(); + }); + }); + do_test_pending(); +} + +function test_simple_breakpoint() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let source = gThreadClient.source(aPacket.frame.where.source); + source.setBreakpoint({ + line: 3, + condition: "throw new Error()" + }, function (aResponse, bpClient) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.why.type, "breakpointConditionThrown"); + do_check_eq(aPacket.frame.where.line, 3); + + // Remove the breakpoint. + bpClient.remove(function (aResponse) { + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + }); + // Continue until the breakpoint is hit. + gThreadClient.resume(); + + }); + + }); + + Components.utils.evalInSandbox("debugger;\n" + // 1 + "var a = 1;\n" + // 2 + "var b = 2;\n", // 3 + gDebuggee, + "1.8", + "test.js", + 1); +} diff --git a/devtools/server/tests/unit/test_dbgactor.js b/devtools/server/tests/unit/test_dbgactor.js new file mode 100644 index 000000000..b22b8446b --- /dev/null +++ b/devtools/server/tests/unit/test_dbgactor.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gClient; +var gDebuggee; + +const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = testGlobal("test-1"); + DebuggerServer.addTestGlobal(gDebuggee); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.addListener("connected", function (aEvent, aType, aTraits) { + gClient.listTabs((aResponse) => { + do_check_true("tabs" in aResponse); + for (let tab of aResponse.tabs) { + if (tab.title == "test-1") { + test_attach_tab(tab.actor); + return false; + } + } + do_check_true(false); // We should have found our tab in the list. + return undefined; + }); + }); + + gClient.connect(); + + do_test_pending(); +} + +// Attach to |aTabActor|, and check the response. +function test_attach_tab(aTabActor) +{ + gClient.request({ to: aTabActor, type: "attach" }, function (aResponse) { + do_check_false("error" in aResponse); + do_check_eq(aResponse.from, aTabActor); + do_check_eq(aResponse.type, "tabAttached"); + do_check_true(typeof aResponse.threadActor === "string"); + + test_attach_thread(aResponse.threadActor); + }); +} + +// Attach to |aThreadActor|, check the response, and resume it. +function test_attach_thread(aThreadActor) +{ + gClient.request({ to: aThreadActor, type: "attach" }, function (aResponse) { + do_check_false("error" in aResponse); + do_check_eq(aResponse.from, aThreadActor); + do_check_eq(aResponse.type, "paused"); + do_check_true("why" in aResponse); + do_check_eq(aResponse.why.type, "attached"); + + test_resume_thread(aThreadActor); + }); +} + +// Resume |aThreadActor|, and see that it stops at the 'debugger' +// statement. +function test_resume_thread(aThreadActor) +{ + // Allow the client to resume execution. + gClient.request({ to: aThreadActor, type: "resume" }, function (aResponse) { + do_check_false("error" in aResponse); + do_check_eq(aResponse.from, aThreadActor); + do_check_eq(aResponse.type, "resumed"); + + do_check_eq(xpcInspector.eventLoopNestLevel, 0); + + // Now that we know we're resumed, we can make the debuggee do something. + Cu.evalInSandbox("var a = true; var b = false; debugger; var b = true;", gDebuggee); + // Now make sure that we've run the code after the debugger statement... + do_check_true(gDebuggee.b); + }); + + gClient.addListener("paused", function (aName, aPacket) { + do_check_eq(aName, "paused"); + do_check_false("error" in aPacket); + do_check_eq(aPacket.from, aThreadActor); + do_check_eq(aPacket.type, "paused"); + do_check_true("actor" in aPacket); + do_check_true("why" in aPacket); + do_check_eq(aPacket.why.type, "debuggerStatement"); + + // Reach around the protocol to check that the debuggee is in the state + // we expect. + do_check_true(gDebuggee.a); + do_check_false(gDebuggee.b); + + do_check_eq(xpcInspector.eventLoopNestLevel, 1); + + // Let the debuggee continue execution. + gClient.request({ to: aThreadActor, type: "resume" }, cleanup); + }); +} + +function cleanup() +{ + gClient.addListener("closed", function (aEvent, aResult) { + do_test_finished(); + }); + + try { + let xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); + do_check_eq(xpcInspector.eventLoopNestLevel, 0); + } catch (e) { + dump(e); + } + + gClient.close(); +} diff --git a/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js b/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js new file mode 100644 index 000000000..40468cb1d --- /dev/null +++ b/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gClient; +var gTabClient; +var gDebuggee; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = testGlobal("test-1"); + DebuggerServer.addTestGlobal(gDebuggee); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect().then(function ([aType, aTraits]) { + attachTestTab(gClient, "test-1", function (aReply, aTabClient) { + gTabClient = aTabClient; + test_threadAttach(aReply.threadActor); + }); + }); + do_test_pending(); +} + +function test_threadAttach(aThreadActorID) +{ + do_print("Trying to attach to thread " + aThreadActorID); + gTabClient.attachThread({}, function (aResponse, aThreadClient) { + do_check_eq(aThreadClient.state, "paused"); + do_check_eq(aThreadClient.actor, aThreadActorID); + aThreadClient.resume(function () { + do_check_eq(aThreadClient.state, "attached"); + test_debugger_statement(aThreadClient); + }); + }); +} + +function test_debugger_statement(aThreadClient) +{ + aThreadClient.addListener("paused", function (aEvent, aPacket) { + do_check_eq(aThreadClient.state, "paused"); + // Reach around the protocol to check that the debuggee is in the state + // we expect. + do_check_true(gDebuggee.a); + do_check_false(gDebuggee.b); + + let xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); + do_check_eq(xpcInspector.eventLoopNestLevel, 1); + + aThreadClient.resume(cleanup); + }); + + Cu.evalInSandbox("var a = true; var b = false; debugger; var b = true;", gDebuggee); + + // Now make sure that we've run the code after the debugger statement... + do_check_true(gDebuggee.b); +} + +function cleanup() +{ + gClient.addListener("closed", function (aEvent) { + do_test_finished(); + }); + + try { + let xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); + do_check_eq(xpcInspector.eventLoopNestLevel, 0); + } catch (e) { + dump(e); + } + + gClient.close(); +} diff --git a/devtools/server/tests/unit/test_dbgglobal.js b/devtools/server/tests/unit/test_dbgglobal.js new file mode 100644 index 000000000..ff4291932 --- /dev/null +++ b/devtools/server/tests/unit/test_dbgglobal.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + // Should get an exception if we try to interact with DebuggerServer + // before we initialize it... + check_except(function () { + DebuggerServer.createListener(); + }); + check_except(DebuggerServer.closeAllListeners); + check_except(DebuggerServer.connectPipe); + + // Allow incoming connections. + DebuggerServer.init(); + + // These should still fail because we haven't added a createRootActor + // implementation yet. + check_except(function () { + DebuggerServer.createListener(); + }); + check_except(DebuggerServer.closeAllListeners); + check_except(DebuggerServer.connectPipe); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + + // Now they should work. + DebuggerServer.createListener(); + DebuggerServer.closeAllListeners(); + + // Make sure we got the test's root actor all set up. + let client1 = DebuggerServer.connectPipe(); + client1.hooks = { + onPacket: function (aPacket1) { + do_check_eq(aPacket1.from, "root"); + do_check_eq(aPacket1.applicationType, "xpcshell-tests"); + + // Spin up a second connection, make sure it has its own root + // actor. + let client2 = DebuggerServer.connectPipe(); + client2.hooks = { + onPacket: function (aPacket2) { + do_check_eq(aPacket2.from, "root"); + do_check_neq(aPacket1.testConnectionPrefix, + aPacket2.testConnectionPrefix); + client2.close(); + }, + onClosed: function (aResult) { + client1.close(); + }, + }; + client2.ready(); + }, + + onClosed: function (aResult) { + do_test_finished(); + }, + }; + + client1.ready(); + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_eval-01.js b/devtools/server/tests/unit/test_eval-01.js new file mode 100644 index 000000000..b11903f87 --- /dev/null +++ b/devtools/server/tests/unit/test_eval-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic eval resume/re-pause + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_eval(); + }); + }); + do_test_pending(); +} + +function test_simple_eval() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let arg1Actor = aPacket.frame.arguments[0].actor; + gThreadClient.eval(null, "({ obj: true })", function (aResponse) { + do_check_eq(aResponse.type, "resumed"); + // Expect a pause notification immediately. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value... + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "clientEvaluated"); + do_check_eq(aPacket.why.frameFinished.return.type, "object"); + do_check_eq(aPacket.why.frameFinished.return.class, "Object"); + + // Make sure the previous pause lifetime was correctly dropped. + gClient.request({ to: arg1Actor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + }); + + }); + + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1) { debugger; } + stopMe({obj: true}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_eval-02.js b/devtools/server/tests/unit/test_eval-02.js new file mode 100644 index 000000000..386ea5c98 --- /dev/null +++ b/devtools/server/tests/unit/test_eval-02.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check eval resume/re-pause with a throw. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_throw_eval(); + }); + }); + do_test_pending(); +} + +function test_throw_eval() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.eval(null, "throw 'failure'", function (aResponse) { + do_check_eq(aResponse.type, "resumed"); + // Expect a pause notification immediately. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value... + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "clientEvaluated"); + do_check_eq(aPacket.why.frameFinished.throw, "failure"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1) { debugger; } + stopMe({obj: true}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_eval-03.js b/devtools/server/tests/unit/test_eval-03.js new file mode 100644 index 000000000..2234259aa --- /dev/null +++ b/devtools/server/tests/unit/test_eval-03.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check syntax errors in an eval. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_syntax_error_eval(); + }); + }); + do_test_pending(); +} + +function test_syntax_error_eval() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.eval(null, "%$@!@#", function (aResponse) { + do_check_eq(aResponse.type, "resumed"); + // Expect a pause notification immediately. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value... + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "clientEvaluated"); + do_check_eq(aPacket.why.frameFinished.throw.type, "object"); + do_check_eq(aPacket.why.frameFinished.throw.class, "Error"); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1) { debugger; } + stopMe({obj: true}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_eval-04.js b/devtools/server/tests/unit/test_eval-04.js new file mode 100644 index 000000000..77cb58d97 --- /dev/null +++ b/devtools/server/tests/unit/test_eval-04.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check evals against different frames. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_syntax_error_eval(); + }); + }); + do_test_pending(); +} + +function test_syntax_error_eval() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + + gThreadClient.getFrames(0, 2, function (aResponse) { + let frame0 = aResponse.frames[0]; + let frame1 = aResponse.frames[1]; + + // Eval against the top frame... + gThreadClient.eval(frame0.actor, "arg", function (aResponse) { + do_check_eq(aResponse.type, "resumed"); + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // 'arg' should have been evaluated in frame0 + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.type, "clientEvaluated"); + do_check_eq(aPacket.why.frameFinished.return, "arg0"); + + // Now eval against the second frame. + gThreadClient.eval(frame1.actor, "arg", function (aResponse) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // 'arg' should have been evaluated in frame1 + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.why.frameFinished.return, "arg1"); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function frame0(arg) { + debugger; + } + function frame1(arg) { + frame0("arg0"); + } + frame1("arg1"); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_eval-05.js b/devtools/server/tests/unit/test_eval-05.js new file mode 100644 index 000000000..b199e4afb --- /dev/null +++ b/devtools/server/tests/unit/test_eval-05.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check pauses within evals. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_syntax_error_eval(); + }); + }); + do_test_pending(); +} + +function test_syntax_error_eval() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.eval(null, "debugger", function (aResponse) { + // Expect a resume then a debuggerStatement pause. + do_check_eq(aResponse.type, "resumed"); + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "debuggerStatement"); + // Resume from the debugger statement should immediately re-pause + // with a clientEvaluated reason. + gThreadClient.resume(function (aPacket) { + do_check_eq(aPacket.type, "resumed"); + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "clientEvaluated"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + }); + }); + gDebuggee.eval("(" + function () { + function stopMe(arg) { + debugger; + } + stopMe(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_eventlooplag_actor.js b/devtools/server/tests/unit/test_eventlooplag_actor.js new file mode 100644 index 000000000..d2acdd8f8 --- /dev/null +++ b/devtools/server/tests/unit/test_eventlooplag_actor.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test the eventLoopLag actor. + */ + +"use strict"; + +function run_test() +{ + let {EventLoopLagFront} = require("devtools/shared/fronts/eventlooplag"); + + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + // As seen in EventTracer.cpp + let threshold = 20; + let interval = 10; + + + let front; + let client = new DebuggerClient(DebuggerServer.connectPipe()); + + // Start tracking event loop lags. + client.connect().then(function () { + client.listTabs(function (resp) { + front = new EventLoopLagFront(client, resp); + front.start().then(success => { + do_check_true(success); + front.once("event-loop-lag", gotLagEvent); + do_execute_soon(lag); + }); + }); + }); + + // Force a lag + function lag() { + let start = new Date(); + let duration = threshold + interval + 1; + while (true) { + if (((new Date()) - start) > duration) { + break; + } + } + } + + // Got a lag event. The test will time out if the actor + // fails to detect the lag. + function gotLagEvent(time) { + do_print("lag: " + time); + do_check_true(time >= threshold); + front.stop().then(() => { + finishClient(client); + }); + } + + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_forwardingprefix.js b/devtools/server/tests/unit/test_forwardingprefix.js new file mode 100644 index 000000000..885a99db8 --- /dev/null +++ b/devtools/server/tests/unit/test_forwardingprefix.js @@ -0,0 +1,196 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Exercise prefix-based forwarding of packets to other transports. */ + +const { RootActor } = require("devtools/server/actors/root"); + +var gMainConnection, gMainTransport; +var gSubconnection1, gSubconnection2; +var gClient; + +function run_test() +{ + DebuggerServer.init(); + + add_test(createMainConnection); + add_test(TestNoForwardingYet); + add_test(createSubconnection1); + add_test(TestForwardPrefix1OnlyRoot); + add_test(createSubconnection2); + add_test(TestForwardPrefix12OnlyRoot); + add_test(TestForwardPrefix12WithActor1); + add_test(TestForwardPrefix12WithActor12); + run_next_test(); +} + +/* + * Create a pipe connection, and return an object |{ conn, transport }|, + * where |conn| is the new DebuggerServerConnection instance, and + * |transport| is the client side of the transport on which it communicates + * (that is, packets sent on |transport| go to the new connection, and + * |transport|'s hooks receive replies). + * + * |aPrefix| is optional; if present, it's the prefix (minus the '/') for + * actors in the new connection. + */ +function newConnection(aPrefix) +{ + var conn; + DebuggerServer.createRootActor = function (aConn) { + conn = aConn; + return new RootActor(aConn, {}); + }; + + var transport = DebuggerServer.connectPipe(aPrefix); + + return { conn: conn, transport: transport }; +} + +/* Create the main connection for these tests. */ +function createMainConnection() +{ + ({ conn: gMainConnection, transport: gMainTransport } = newConnection()); + gClient = new DebuggerClient(gMainTransport); + gClient.connect().then(([aType, aTraits]) => run_next_test()); +} + +/* + * Exchange 'echo' messages with five actors: + * - root + * - prefix1/root + * - prefix1/actor + * - prefix2/root + * - prefix2/actor + * + * Expect proper echos from those named in |aReachables|, and 'noSuchActor' + * errors from the others. When we've gotten all our replies (errors or + * otherwise), call |aCompleted|. + * + * To avoid deep stacks, we call aCompleted from the next tick. + */ +function tryActors(aReachables, aCompleted) { + let count = 0; + + let outerActor; + for (outerActor of [ "root", + "prefix1/root", "prefix1/actor", + "prefix2/root", "prefix2/actor" ]) { + /* + * Let each callback capture its own iteration's value; outerActor is + * local to the whole loop, not to a single iteration. + */ + let actor = outerActor; + + count++; + + gClient.request({ to: actor, type: "echo", value: "tango"}, // phone home + (aResponse) => { + if (aReachables.has(actor)) + do_check_matches({ from: actor, to: actor, type: "echo", value: "tango" }, aResponse); + else + do_check_matches({ from: actor, error: "noSuchActor", message: "No such actor for ID: " + actor }, aResponse); + + if (--count == 0) + do_execute_soon(aCompleted, "tryActors callback " + aCompleted.name); + }); + } +} + +/* + * With no forwarding established, sending messages to root should work, + * but sending messages to prefixed actor names, or anyone else, should get + * an error. + */ +function TestNoForwardingYet() +{ + tryActors(new Set(["root"]), run_next_test); +} + +/* + * Create a new pipe connection which forwards its reply packets to + * gMainConnection's client, and to which gMainConnection forwards packets + * directed to actors whose names begin with |aPrefix + '/'|, and. + * + * Return an object { conn, transport }, as for newConnection. + */ +function newSubconnection(aPrefix) +{ + let { conn, transport } = newConnection(aPrefix); + transport.hooks = { + onPacket: (aPacket) => gMainConnection.send(aPacket), + onClosed: () => {} + }; + gMainConnection.setForwarding(aPrefix, transport); + + return { conn: conn, transport: transport }; +} + +/* Create a second root actor, to which we can forward things. */ +function createSubconnection1() +{ + let { conn, transport } = newSubconnection("prefix1"); + gSubconnection1 = conn; + transport.ready(); + gClient.expectReply("prefix1/root", (aReply) => run_next_test()); +} + +// Establish forwarding, but don't put any actors in that server. +function TestForwardPrefix1OnlyRoot() +{ + tryActors(new Set(["root", "prefix1/root"]), run_next_test); +} + +/* Create a third root actor, to which we can forward things. */ +function createSubconnection2() +{ + let { conn, transport } = newSubconnection("prefix2"); + gSubconnection2 = conn; + transport.ready(); + gClient.expectReply("prefix2/root", (aReply) => run_next_test()); +} + +function TestForwardPrefix12OnlyRoot() +{ + tryActors(new Set(["root", "prefix1/root", "prefix2/root"]), run_next_test); +} + +// A dumb actor that implements 'echo'. +// +// It's okay that both subconnections' actors behave identically, because +// the reply-sending code attaches the replying actor's name to the packet, +// so simply matching the 'from' field in the reply ensures that we heard +// from the right actor. +function EchoActor(aConnection) +{ + this.conn = aConnection; +} +EchoActor.prototype.actorPrefix = "EchoActor"; +EchoActor.prototype.onEcho = function (aRequest) { + /* + * Request packets are frozen. Copy aRequest, so that + * DebuggerServerConnection.onPacket can attach a 'from' property. + */ + return JSON.parse(JSON.stringify(aRequest)); +}; +EchoActor.prototype.requestTypes = { + "echo": EchoActor.prototype.onEcho +}; + +function TestForwardPrefix12WithActor1() +{ + let actor = new EchoActor(gSubconnection1); + actor.actorID = "prefix1/actor"; + gSubconnection1.addActor(actor); + + tryActors(new Set(["root", "prefix1/root", "prefix1/actor", "prefix2/root"]), run_next_test); +} + +function TestForwardPrefix12WithActor12() +{ + let actor = new EchoActor(gSubconnection2); + actor.actorID = "prefix2/actor"; + gSubconnection2.addActor(actor); + + tryActors(new Set(["root", "prefix1/root", "prefix1/actor", "prefix2/root", "prefix2/actor"]), run_next_test); +} diff --git a/devtools/server/tests/unit/test_frameactor-01.js b/devtools/server/tests/unit/test_frameactor-01.js new file mode 100644 index 000000000..ad37a8ab5 --- /dev/null +++ b/devtools/server/tests/unit/test_frameactor-01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that we get a frame actor along with a debugger statement. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_true(!!aPacket.frame); + do_check_true(!!aPacket.frame.actor); + do_check_eq(aPacket.frame.callee.name, "stopMe"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe() { + debugger; + } + stopMe(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_frameactor-02.js b/devtools/server/tests/unit/test_frameactor-02.js new file mode 100644 index 000000000..f2890adac --- /dev/null +++ b/devtools/server/tests/unit/test_frameactor-02.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that two pauses in a row will keep the same frame actor. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket1) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket2) { + do_check_eq(aPacket1.frame.actor, aPacket2.frame.actor); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + gThreadClient.resume(); + }); + + gDebuggee.eval("(" + function () { + function stopMe() { + debugger; + debugger; + } + stopMe(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_frameactor-03.js b/devtools/server/tests/unit/test_frameactor-03.js new file mode 100644 index 000000000..0d7739d5a --- /dev/null +++ b/devtools/server/tests/unit/test_frameactor-03.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that a frame actor is properly expired when the frame goes away. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket1) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket2) { + let poppedFrames = aPacket2.poppedFrames; + do_check_eq(typeof (poppedFrames), typeof ([])); + do_check_true(poppedFrames.indexOf(aPacket1.frame.actor) >= 0); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + gThreadClient.resume(); + }); + + gDebuggee.eval("(" + function () { + function stopMe() { + debugger; + } + stopMe(); + debugger; + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_frameactor-04.js b/devtools/server/tests/unit/test_frameactor-04.js new file mode 100644 index 000000000..b4faa96e0 --- /dev/null +++ b/devtools/server/tests/unit/test_frameactor-04.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify the "frames" request on the thread. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +var gFrames = [ + // Function calls... + { type: "call", callee: { name: "depth3" } }, + { type: "call", callee: { name: "depth2" } }, + { type: "call", callee: { name: "depth1" } }, + + // Anonymous function call in our eval... + { type: "call", callee: { name: undefined } }, + + // The eval itself. + { type: "eval", callee: { name: undefined } }, +]; + +var gSliceTests = [ + { start: 0, count: undefined, resetActors: true }, + { start: 0, count: 1 }, + { start: 2, count: 2 }, + { start: 1, count: 15 }, + { start: 15, count: undefined }, +]; + +function test_frame_slice() { + if (gSliceTests.length == 0) { + gThreadClient.resume(function () { finishClient(gClient); }); + return; + } + + let test = gSliceTests.shift(); + gThreadClient.getFrames(test.start, test.count, function (aResponse) { + var testFrames = gFrames.slice(test.start, test.count ? test.start + test.count : undefined); + do_check_eq(testFrames.length, aResponse.frames.length); + for (var i = 0; i < testFrames.length; i++) { + let expected = testFrames[i]; + let actual = aResponse.frames[i]; + + if (test.resetActors) { + expected.actor = actual.actor; + } + + for (let key of ["type", "callee-name"]) { + do_check_eq(expected[key] || undefined, actual[key]); + } + } + test_frame_slice(); + }); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket1) { + test_frame_slice(); + }); + + gDebuggee.eval("(" + function () { + function depth3() { + debugger; + } + function depth2() { + depth3(); + } + function depth1() { + depth2(); + } + depth1(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_frameactor-05.js b/devtools/server/tests/unit/test_frameactor-05.js new file mode 100644 index 000000000..feece598e --- /dev/null +++ b/devtools/server/tests/unit/test_frameactor-05.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that frame actors retrieved with the frames request + * are included in the pause packet's popped-frames property. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_frame_slice() { + if (gSliceTests.length == 0) { + gThreadClient.resume(function () { finishClient(gClient); }); + return; + } + + let test = gSliceTests.shift(); + gThreadClient.getFrames(test.start, test.count, function (aResponse) { + var testFrames = gFrames.slice(test.start, test.count ? test.start + test.count : undefined); + do_check_eq(testFrames.length, aResponse.frames.length); + for (var i = 0; i < testFrames.length; i++) { + let expected = testFrames[i]; + let actual = aResponse.frames[i]; + + if (test.resetActors) { + expected.actor = actual.actor; + } + + for (var key in expected) { + do_check_eq(expected[key], actual[key]); + } + } + test_frame_slice(); + }); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket1) { + gThreadClient.getFrames(0, null, function (aFrameResponse) { + do_check_eq(aFrameResponse.frames.length, 5); + // Now wait for the next pause, after which the three + // youngest actors should be popped.. + let expectPopped = aFrameResponse.frames.slice(0, 3).map(frame => frame.actor); + expectPopped.sort(); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPausePacket) { + let popped = aPausePacket.poppedFrames.sort(); + do_check_eq(popped.length, 3); + for (let i = 0; i < 3; i++) { + do_check_eq(expectPopped[i], popped[i]); + } + + gThreadClient.resume(function () { finishClient(gClient); }); + }); + gThreadClient.resume(); + }); + }); + + gDebuggee.eval("(" + function () { + function depth3() { + debugger; + } + function depth2() { + depth3(); + } + function depth1() { + depth2(); + } + depth1(); + debugger; + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_framearguments-01.js b/devtools/server/tests/unit/test_framearguments-01.js new file mode 100644 index 000000000..e075d2c25 --- /dev/null +++ b/devtools/server/tests/unit/test_framearguments-01.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check a frame actor's arguments property. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame["arguments"]; + do_check_eq(args.length, 6); + do_check_eq(args[0], 42); + do_check_eq(args[1], true); + do_check_eq(args[2], "nasu"); + do_check_eq(args[3].type, "null"); + do_check_eq(args[4].type, "undefined"); + do_check_eq(args[5].type, "object"); + do_check_eq(args[5].class, "Object"); + do_check_true(!!args[5].actor); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aNumber, aBool, aString, aNull, aUndefined, aObject) { + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_framebindings-01.js b/devtools/server/tests/unit/test_framebindings-01.js new file mode 100644 index 000000000..ae62f8ff1 --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-01.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check a frame actor's bindings property. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let bindings = aPacket.frame.environment.bindings; + let args = bindings.arguments; + let vars = bindings.variables; + + do_check_eq(args.length, 6); + do_check_eq(args[0].aNumber.value, 42); + do_check_eq(args[1].aBool.value, true); + do_check_eq(args[2].aString.value, "nasu"); + do_check_eq(args[3].aNull.value.type, "null"); + do_check_eq(args[4].aUndefined.value.type, "undefined"); + do_check_eq(args[5].aObject.value.type, "object"); + do_check_eq(args[5].aObject.value.class, "Object"); + do_check_true(!!args[5].aObject.value.actor); + + do_check_eq(vars.a.value, 1); + do_check_eq(vars.b.value, true); + do_check_eq(vars.c.value.type, "object"); + do_check_eq(vars.c.value.class, "Object"); + do_check_true(!!vars.c.value.actor); + + let objClient = gThreadClient.pauseGrip(vars.c.value); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.a.configurable, true); + do_check_eq(aResponse.ownProperties.a.enumerable, true); + do_check_eq(aResponse.ownProperties.a.writable, true); + do_check_eq(aResponse.ownProperties.a.value, "a"); + + do_check_eq(aResponse.ownProperties.b.configurable, true); + do_check_eq(aResponse.ownProperties.b.enumerable, true); + do_check_eq(aResponse.ownProperties.b.writable, true); + do_check_eq(aResponse.ownProperties.b.value.type, "undefined"); + do_check_false("class" in aResponse.ownProperties.b.value); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aNumber, aBool, aString, aNull, aUndefined, aObject) { + var a = 1; + var b = true; + var c = { a: "a", b: undefined }; + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_framebindings-02.js b/devtools/server/tests/unit/test_framebindings-02.js new file mode 100644 index 000000000..552670349 --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-02.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check a frame actor's parent bindings. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let parentEnv = aPacket.frame.environment.parent; + let bindings = parentEnv.bindings; + let args = bindings.arguments; + let vars = bindings.variables; + do_check_neq(parentEnv, undefined); + do_check_eq(args.length, 0); + do_check_eq(vars.stopMe.value.type, "object"); + do_check_eq(vars.stopMe.value.class, "Function"); + do_check_true(!!vars.stopMe.value.actor); + + // Skip the global lexical scope. + parentEnv = parentEnv.parent.parent; + do_check_neq(parentEnv, undefined); + let objClient = gThreadClient.pauseGrip(parentEnv.object); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.Object.value.type, "object"); + do_check_eq(aResponse.ownProperties.Object.value.class, "Function"); + do_check_true(!!aResponse.ownProperties.Object.value.actor); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aNumber, aBool, aString, aNull, aUndefined, aObject) { + var a = 1; + var b = true; + var c = { a: "a" }; + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_framebindings-03.js b/devtools/server/tests/unit/test_framebindings-03.js new file mode 100644 index 000000000..1ec51570d --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-03.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check a |with| frame actor's bindings. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let env = aPacket.frame.environment; + do_check_neq(env, undefined); + + let parentEnv = env.parent; + do_check_neq(parentEnv, undefined); + + let bindings = parentEnv.bindings; + let args = bindings.arguments; + let vars = bindings.variables; + do_check_eq(args.length, 1); + do_check_eq(args[0].aNumber.value, 10); + do_check_eq(vars.r.value, 10); + do_check_eq(vars.a.value, Math.PI * 100); + do_check_eq(vars.arguments.value.class, "Arguments"); + do_check_true(!!vars.arguments.value.actor); + + let objClient = gThreadClient.pauseGrip(env.object); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.PI.value, Math.PI); + do_check_eq(aResponse.ownProperties.cos.value.type, "object"); + do_check_eq(aResponse.ownProperties.cos.value.class, "Function"); + do_check_true(!!aResponse.ownProperties.cos.value.actor); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aNumber) { + var a; + var r = aNumber; + with (Math) { + a = PI * r * r; + debugger; + } + } + stopMe(10); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_framebindings-04.js b/devtools/server/tests/unit/test_framebindings-04.js new file mode 100644 index 000000000..963a12055 --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-04.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check the environment bindongs of a |with| within a |with|. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let env = aPacket.frame.environment; + do_check_neq(env, undefined); + + let objClient = gThreadClient.pauseGrip(env.object); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.one.value, 1); + do_check_eq(aResponse.ownProperties.two.value, 2); + do_check_eq(aResponse.ownProperties.foo, undefined); + + let parentEnv = env.parent; + do_check_neq(parentEnv, undefined); + + let parentClient = gThreadClient.pauseGrip(parentEnv.object); + parentClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.PI.value, Math.PI); + do_check_eq(aResponse.ownProperties.cos.value.type, "object"); + do_check_eq(aResponse.ownProperties.cos.value.class, "Function"); + do_check_true(!!aResponse.ownProperties.cos.value.actor); + + parentEnv = parentEnv.parent; + do_check_neq(parentEnv, undefined); + + let bindings = parentEnv.bindings; + let args = bindings.arguments; + let vars = bindings.variables; + do_check_eq(args.length, 1); + do_check_eq(args[0].aNumber.value, 10); + do_check_eq(vars.r.value, 10); + do_check_eq(vars.a.value, Math.PI * 100); + do_check_eq(vars.arguments.value.class, "Arguments"); + do_check_true(!!vars.arguments.value.actor); + do_check_eq(vars.foo.value, 2 * Math.PI); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + }); + + gDebuggee.eval("(" + function () { + function stopMe(aNumber) { + var a, obj = { one: 1, two: 2 }; + var r = aNumber; + with (Math) { + a = PI * r * r; + with (obj) { + var foo = two * PI; + debugger; + } + } + } + stopMe(10); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_framebindings-05.js b/devtools/server/tests/unit/test_framebindings-05.js new file mode 100644 index 000000000..9827c617a --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-05.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check the environment bindings of a |with| in global scope. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let env = aPacket.frame.environment; + do_check_neq(env, undefined); + + let objClient = gThreadClient.pauseGrip(env.object); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.PI.value, Math.PI); + do_check_eq(aResponse.ownProperties.cos.value.type, "object"); + do_check_eq(aResponse.ownProperties.cos.value.class, "Function"); + do_check_true(!!aResponse.ownProperties.cos.value.actor); + + // Skip the global lexical scope. + let parentEnv = env.parent.parent; + do_check_neq(parentEnv, undefined); + + let parentClient = gThreadClient.pauseGrip(parentEnv.object); + parentClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.a.value, Math.PI * 100); + do_check_eq(aResponse.ownProperties.r.value, 10); + do_check_eq(aResponse.ownProperties.Object.value.type, "object"); + do_check_eq(aResponse.ownProperties.Object.value.class, "Function"); + do_check_true(!!aResponse.ownProperties.Object.value.actor); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + + gDebuggee.eval("var a, r = 10;\n" + + "with (Math) {\n" + + " a = PI * r * r;\n" + + " debugger;\n" + + "}"); +} diff --git a/devtools/server/tests/unit/test_framebindings-06.js b/devtools/server/tests/unit/test_framebindings-06.js new file mode 100644 index 000000000..9d8478a29 --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-06.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_banana_environment(); + }); + }); + do_test_pending(); +} + +function test_banana_environment() +{ + + gThreadClient.addOneTimeListener("paused", + function (aEvent, aPacket) { + equal(aPacket.type, "paused"); + let env = aPacket.frame.environment; + equal(env.type, "function"); + equal(env.function.name, "banana3"); + let parent = env.parent; + equal(parent.type, "block"); + ok("banana3" in parent.bindings.variables); + parent = parent.parent; + equal(parent.type, "function"); + equal(parent.function.name, "banana2"); + parent = parent.parent; + equal(parent.type, "block"); + ok("banana2" in parent.bindings.variables); + parent = parent.parent; + equal(parent.type, "function"); + equal(parent.function.name, "banana"); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + gDebuggee.eval("\ + function banana(x) { \n\ + return function banana2(y) { \n\ + return function banana3(z) { \n\ + debugger; \n\ + }; \n\ + }; \n\ + } \n\ + banana('x')('y')('z'); \n\ + "); +} diff --git a/devtools/server/tests/unit/test_framebindings-07.js b/devtools/server/tests/unit/test_framebindings-07.js new file mode 100644 index 000000000..bdfc36d97 --- /dev/null +++ b/devtools/server/tests/unit/test_framebindings-07.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +// Test that the EnvironmentClient's getBindings() method works as expected. +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-bindings"); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-bindings", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_banana_environment(); + }); + }); + do_test_pending(); +} + +function test_banana_environment() +{ + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let environment = aPacket.frame.environment; + do_check_eq(environment.type, "function"); + + let parent = environment.parent; + do_check_eq(parent.type, "block"); + + let grandpa = parent.parent; + do_check_eq(grandpa.type, "function"); + + let envClient = gThreadClient.environment(environment); + envClient.getBindings(aResponse => { + do_check_eq(aResponse.bindings.arguments[0].z.value, "z"); + + let parentClient = gThreadClient.environment(parent); + parentClient.getBindings(aResponse => { + do_check_eq(aResponse.bindings.variables.banana3.value.class, "Function"); + + let grandpaClient = gThreadClient.environment(grandpa); + grandpaClient.getBindings(aResponse => { + do_check_eq(aResponse.bindings.arguments[0].y.value, "y"); + gThreadClient.resume(() => finishClient(gClient)); + }); + }); + }); + }); + + gDebuggee.eval("\ + function banana(x) { \n\ + return function banana2(y) { \n\ + return function banana3(z) { \n\ + debugger; \n\ + }; \n\ + }; \n\ + } \n\ + banana('x')('y')('z'); \n\ + "); +} diff --git a/devtools/server/tests/unit/test_frameclient-01.js b/devtools/server/tests/unit/test_frameclient-01.js new file mode 100644 index 000000000..a441c9ade --- /dev/null +++ b/devtools/server/tests/unit/test_frameclient-01.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("framesadded", function () { + do_check_eq(gThreadClient.cachedFrames.length, 3); + do_check_true(gThreadClient.moreFrames); + do_check_false(gThreadClient.fillFrames(3)); + + do_check_true(gThreadClient.fillFrames(30)); + gThreadClient.addOneTimeListener("framesadded", function () { + do_check_false(gThreadClient.moreFrames); + do_check_eq(gThreadClient.cachedFrames.length, 7); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + do_check_true(gThreadClient.fillFrames(3)); + }); + + gDebuggee.eval("(" + function () { + var recurseLeft = 5; + function recurse() { + if (--recurseLeft == 0) { + debugger; + return; + } + recurse(); + } + recurse(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_frameclient-02.js b/devtools/server/tests/unit/test_frameclient-02.js new file mode 100644 index 000000000..a257e5960 --- /dev/null +++ b/devtools/server/tests/unit/test_frameclient-02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Ask for exactly the number of frames we expect. + gThreadClient.addOneTimeListener("framesadded", function () { + do_check_false(gThreadClient.moreFrames); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + do_check_true(gThreadClient.fillFrames(3)); + }); + + gDebuggee.eval("(" + function () { + var recurseLeft = 1; + function recurse() { + if (--recurseLeft == 0) { + debugger; + return; + } + recurse(); + } + recurse(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_functiongrips-01.js b/devtools/server/tests/unit/test_functiongrips-01.js new file mode 100644 index 000000000..c41a7cad5 --- /dev/null +++ b/devtools/server/tests/unit/test_functiongrips-01.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_named_function(); + }); + }); + do_test_pending(); +} + +function test_named_function() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Function"); + do_check_eq(args[0].name, "stopMe"); + do_check_eq(args[0].displayName, "stopMe"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getParameterNames(function (aResponse) { + do_check_eq(aResponse.parameterNames.length, 1); + do_check_eq(aResponse.parameterNames[0], "arg1"); + + gThreadClient.resume(test_inferred_name_function); + }); + + }); + + gDebuggee.eval("stopMe(stopMe)"); +} + +function test_inferred_name_function() { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Function"); + // No name for an anonymous function, but it should have an inferred name. + do_check_eq(args[0].name, undefined); + do_check_eq(args[0].displayName, "o.m"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getParameterNames(function (aResponse) { + do_check_eq(aResponse.parameterNames.length, 3); + do_check_eq(aResponse.parameterNames[0], "foo"); + do_check_eq(aResponse.parameterNames[1], "bar"); + do_check_eq(aResponse.parameterNames[2], "baz"); + + gThreadClient.resume(test_anonymous_function); + }); + }); + + gDebuggee.eval("var o = { m: function(foo, bar, baz) { } }; stopMe(o.m)"); +} + +function test_anonymous_function() { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Function"); + // No name for an anonymous function, and no inferred name, either. + do_check_eq(args[0].name, undefined); + do_check_eq(args[0].displayName, undefined); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getParameterNames(function (aResponse) { + do_check_eq(aResponse.parameterNames.length, 3); + do_check_eq(aResponse.parameterNames[0], "foo"); + do_check_eq(aResponse.parameterNames[1], "bar"); + do_check_eq(aResponse.parameterNames[2], "baz"); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + gDebuggee.eval("stopMe(function(foo, bar, baz) { })"); +} + diff --git a/devtools/server/tests/unit/test_get-executable-lines-source-map.js b/devtools/server/tests/unit/test_get-executable-lines-source-map.js new file mode 100644 index 000000000..bca8eebee --- /dev/null +++ b/devtools/server/tests/unit/test_get-executable-lines-source-map.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test if getExecutableLines return correct information + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const SOURCE_MAPPED_FILE = getFileUrl("sourcemapped.js"); + +function run_test() { + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-get-executable-lines"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function _onConnect() { + attachTestTabAndResume( + gClient, + "test-get-executable-lines", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_executable_lines(); + } + ); + }); + + do_test_pending(); +} + +function test_executable_lines() { + gThreadClient.addOneTimeListener("newSource", function _onNewSource(evt, packet) { + do_check_eq(evt, "newSource"); + + gThreadClient.getSources(function ({error, sources}) { + do_check_true(!error); + let source = gThreadClient.source(sources[0]); + source.getExecutableLines(function (lines) { + do_check_true(arrays_equal([1, 2, 4, 6], lines)); + finishClient(gClient); + }); + }); + }); + + let code = readFile("sourcemapped.js") + "\n//# sourceMappingURL=" + + getFileUrl("source-map-data/sourcemapped.map"); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + SOURCE_MAPPED_FILE, 1); +} + +function arrays_equal(a, b) { + return !(a < b || b < a); +} diff --git a/devtools/server/tests/unit/test_get-executable-lines.js b/devtools/server/tests/unit/test_get-executable-lines.js new file mode 100644 index 000000000..233fb6ada --- /dev/null +++ b/devtools/server/tests/unit/test_get-executable-lines.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test if getExecutableLines return correct information + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const SOURCE_MAPPED_FILE = getFileUrl("sourcemapped.js"); + +function run_test() { + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-get-executable-lines"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function _onConnect() { + attachTestTabAndResume( + gClient, + "test-get-executable-lines", + function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_executable_lines(); + } + ); + }); + + do_test_pending(); +} + +function test_executable_lines() { + gThreadClient.addOneTimeListener("newSource", function _onNewSource(evt, packet) { + do_check_eq(evt, "newSource"); + + gThreadClient.getSources(function ({error, sources}) { + do_check_true(!error); + let source = gThreadClient.source(sources[0]); + source.getExecutableLines(function (lines) { + do_check_true(arrays_equal([2, 5, 7, 8, 10, 12, 14, 16], lines)); + finishClient(gClient); + }); + }); + }); + + let code = readFile("sourcemapped.js"); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + SOURCE_MAPPED_FILE, 1); +} + +function arrays_equal(a, b) { + return !(a < b || b < a); +} diff --git a/devtools/server/tests/unit/test_getRuleText.js b/devtools/server/tests/unit/test_getRuleText.js new file mode 100644 index 000000000..fe735928d --- /dev/null +++ b/devtools/server/tests/unit/test_getRuleText.js @@ -0,0 +1,137 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {getRuleText} = require("devtools/server/actors/styles"); + +const TEST_DATA = [ + { + desc: "Empty input", + input: "", + line: 1, + column: 1, + throws: true + }, + { + desc: "Simplest test case", + input: "#id{color:red;background:yellow;}", + line: 1, + column: 1, + expected: {offset: 4, text: "color:red;background:yellow;"} + }, + { + desc: "Multiple rules test case", + input: "#id{color:red;background:yellow;}.class-one .class-two { position:absolute; line-height: 45px}", + line: 1, + column: 34, + expected: {offset: 56, text: " position:absolute; line-height: 45px"} + }, + { + desc: "Unclosed rule", + input: "#id{color:red;background:yellow;", + line: 1, + column: 1, + expected: {offset: 4, text: "color:red;background:yellow;"} + }, + { + desc: "Null input", + input: null, + line: 1, + column: 1, + throws: true + }, + { + desc: "Missing loc", + input: "#id{color:red;background:yellow;}", + throws: true + }, + { + desc: "Multi-lines CSS", + input: [ + "/* this is a multi line css */", + "body {", + " color: green;", + " background-repeat: no-repeat", + "}", + " /*something else here */", + "* {", + " color: purple;", + "}" + ].join("\n"), + line: 7, + column: 1, + expected: {offset: 116, text: "\n color: purple;\n"} + }, + { + desc: "Multi-lines CSS and multi-line rule", + input: [ + "/* ", + "* some comments", + "*/", + "", + "body {", + " margin: 0;", + " padding: 15px 15px 2px 15px;", + " color: red;", + "}", + "", + "#header .btn, #header .txt {", + " font-size: 100%;", + "}", + "", + "#header #information {", + " color: #dddddd;", + " font-size: small;", + "}", + ].join("\n"), + line: 5, + column: 1, + expected: { + offset: 30, + text: "\n margin: 0;\n padding: 15px 15px 2px 15px;\n color: red;\n"} + }, + { + desc: "Content string containing a } character", + input: " #id{border:1px solid red;content: '}';color:red;}", + line: 1, + column: 4, + expected: {offset: 7, text: "border:1px solid red;content: '}';color:red;"} + }, + { + desc: "Rule contains no tokens", + input: "div{}", + line: 1, + column: 1, + expected: {offset: 4, text: ""} + }, +]; + +function run_test() { + for (let test of TEST_DATA) { + do_print("Starting test: " + test.desc); + do_print("Input string " + test.input); + let output; + try { + output = getRuleText(test.input, test.line, test.column); + if (test.throws) { + do_print("Test should have thrown"); + do_check_true(false); + } + } catch (e) { + do_print("getRuleText threw an exception with the given input string"); + if (test.throws) { + do_print("Exception expected"); + do_check_true(true); + } else { + do_print("Exception unexpected\n" + e); + do_check_true(false); + } + } + if (output) { + deepEqual(output, test.expected); + } + } +} diff --git a/devtools/server/tests/unit/test_getTextAtLineColumn.js b/devtools/server/tests/unit/test_getTextAtLineColumn.js new file mode 100644 index 000000000..16ec47608 --- /dev/null +++ b/devtools/server/tests/unit/test_getTextAtLineColumn.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {getTextAtLineColumn} = require("devtools/server/actors/styles"); + +const TEST_DATA = [ + { + desc: "simplest", + input: "#id{color:red;background:yellow;}", + line: 1, + column: 5, + expected: {offset: 4, text: "color:red;background:yellow;}"} + }, + { + desc: "multiple lines", + input: "one\n two\n three", + line: 3, + column: 3, + expected: {offset: 11, text: "three"} + }, +]; + +function run_test() { + for (let test of TEST_DATA) { + do_print("Starting test: " + test.desc); + do_print("Input string " + test.input); + + let output = getTextAtLineColumn(test.input, test.line, test.column); + deepEqual(output, test.expected); + } +} diff --git a/devtools/server/tests/unit/test_getyoungestframe.js b/devtools/server/tests/unit/test_getyoungestframe.js new file mode 100644 index 000000000..035ab5b0c --- /dev/null +++ b/devtools/server/tests/unit/test_getyoungestframe.js @@ -0,0 +1,30 @@ +function run_test() +{ + Components.utils.import("resource://gre/modules/jsdebugger.jsm"); + addDebuggerToGlobal(this); + var xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); + var g = testGlobal("test1"); + + var dbg = new Debugger(); + dbg.uncaughtExceptionHook = testExceptionHook; + + dbg.addDebuggee(g); + dbg.onDebuggerStatement = function (aFrame) { + do_check_true(aFrame === dbg.getNewestFrame()); + // Execute from the nested event loop, dbg.getNewestFrame() won't + // be working anymore. + + do_execute_soon(function () { + try { + do_check_true(aFrame === dbg.getNewestFrame()); + } finally { + xpcInspector.exitNestedEventLoop("test"); + } + }); + xpcInspector.enterNestedEventLoop("test"); + }; + + g.eval("function debuggerStatement() { debugger; }; debuggerStatement();"); + + dbg.enabled = false; +} diff --git a/devtools/server/tests/unit/test_ignore_caught_exceptions.js b/devtools/server/tests/unit/test_ignore_caught_exceptions.js new file mode 100644 index 000000000..a4b221823 --- /dev/null +++ b/devtools/server/tests/unit/test_ignore_caught_exceptions.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that setting ignoreCaughtExceptions will cause the debugger to ignore + * caught exceptions, but not uncaught ones. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "exception"); + do_check_eq(aPacket.why.exception, "bar"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + gThreadClient.pauseOnExceptions(true, true); + gThreadClient.resume(); + }); + + try { + gDebuggee.eval("(" + function () { + debugger; + try { + throw "foo"; + } catch (e) {} + throw "bar"; + } + ")()"); + } catch (e) {} +} diff --git a/devtools/server/tests/unit/test_ignore_no_interface_exceptions.js b/devtools/server/tests/unit/test_ignore_no_interface_exceptions.js new file mode 100644 index 000000000..5aaa31de3 --- /dev/null +++ b/devtools/server/tests/unit/test_ignore_no_interface_exceptions.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the debugger automatically ignores NS_ERROR_NO_INTERFACE + * exceptions, but not normal ones. + */ + + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-no-interface"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-no-interface", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.pauseOnExceptions(true, false, function () { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "exception"); + do_check_eq(aPacket.why.exception, 42); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + gDebuggee.eval("(" + function () { + function QueryInterface() { + throw Components.results.NS_ERROR_NO_INTERFACE; + } + function stopMe() { + throw 42; + } + try { + QueryInterface(); + } catch (e) {} + try { + stopMe(); + } catch (e) {} + } + ")()"); + }); +} diff --git a/devtools/server/tests/unit/test_interrupt.js b/devtools/server/tests/unit/test_interrupt.js new file mode 100644 index 000000000..34835cc0a --- /dev/null +++ b/devtools/server/tests/unit/test_interrupt.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gClient; +var gDebuggee; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = testGlobal("test-1"); + DebuggerServer.addTestGlobal(gDebuggee); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect().then(function (aType, aTraits) { + attachTestTab(gClient, "test-1", test_attach); + }); + do_test_pending(); +} + +function test_attach(aResponse, aTabClient) +{ + aTabClient.attachThread({}, function (aResponse, aThreadClient) { + do_check_eq(aThreadClient.paused, true); + aThreadClient.resume(function () { + test_interrupt(aThreadClient); + }); + }); +} + +function test_interrupt(aThreadClient) +{ + do_check_eq(aThreadClient.paused, false); + aThreadClient.interrupt(function (aResponse) { + do_check_eq(aThreadClient.paused, true); + aThreadClient.resume(function () { + do_check_eq(aThreadClient.paused, false); + cleanup(); + }); + }); +} + +function cleanup() +{ + gClient.addListener("closed", function (aEvent) { + do_test_finished(); + }); + gClient.close(); +} + diff --git a/devtools/server/tests/unit/test_layout-reflows-observer.js b/devtools/server/tests/unit/test_layout-reflows-observer.js new file mode 100644 index 000000000..ff6c07b26 --- /dev/null +++ b/devtools/server/tests/unit/test_layout-reflows-observer.js @@ -0,0 +1,286 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the LayoutChangesObserver + +var { + getLayoutChangesObserver, + releaseLayoutChangesObserver, + LayoutChangesObserver +} = require("devtools/server/actors/reflow"); + +// Override set/clearTimeout on LayoutChangesObserver to avoid depending on +// time in this unit test. This means that LayoutChangesObserver.eventLoopTimer +// will be the timeout callback instead of the timeout itself, so test cases +// will need to execute it to fake a timeout +LayoutChangesObserver.prototype._setTimeout = cb => cb; +LayoutChangesObserver.prototype._clearTimeout = function () {}; + +// Mock the tabActor since we only really want to test the LayoutChangesObserver +// and don't want to depend on a window object, nor want to test protocol.js +function MockTabActor() { + this.window = new MockWindow(); + this.windows = [this.window]; + this.attached = true; +} + +function MockWindow() {} +MockWindow.prototype = { + QueryInterface: function () { + let self = this; + return { + getInterface: function () { + return { + QueryInterface: function () { + if (!self.docShell) { + self.docShell = new MockDocShell(); + } + return self.docShell; + } + }; + } + }; + }, + setTimeout: function (cb) { + // Simply return the cb itself so that we can execute it in the test instead + // of depending on a real timeout + return cb; + }, + clearTimeout: function () {} +}; + +function MockDocShell() { + this.observer = null; +} +MockDocShell.prototype = { + addWeakReflowObserver: function (observer) { + this.observer = observer; + }, + removeWeakReflowObserver: function () {}, + get chromeEventHandler() { + return { + addEventListener: (type, cb) => { + if (type === "resize") { + this.resizeCb = cb; + } + }, + removeEventListener: (type, cb) => { + if (type === "resize" && cb === this.resizeCb) { + this.resizeCb = null; + } + } + }; + }, + mockResize: function () { + if (this.resizeCb) { + this.resizeCb(); + } + } +}; + +function run_test() { + instancesOfObserversAreSharedBetweenWindows(); + eventsAreBatched(); + noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts(); + observerIsAlreadyStarted(); + destroyStopsObserving(); + stoppingAndStartingSeveralTimesWorksCorrectly(); + reflowsArentStackedWhenStopped(); + stackedReflowsAreResetOnStop(); +} + +function instancesOfObserversAreSharedBetweenWindows() { + do_print("Checking that when requesting twice an instances of the observer " + + "for the same TabActor, the instance is shared"); + + do_print("Checking 2 instances of the observer for the tabActor 1"); + let tabActor1 = new MockTabActor(); + let obs11 = getLayoutChangesObserver(tabActor1); + let obs12 = getLayoutChangesObserver(tabActor1); + do_check_eq(obs11, obs12); + + do_print("Checking 2 instances of the observer for the tabActor 2"); + let tabActor2 = new MockTabActor(); + let obs21 = getLayoutChangesObserver(tabActor2); + let obs22 = getLayoutChangesObserver(tabActor2); + do_check_eq(obs21, obs22); + + do_print("Checking that observers instances for 2 different tabActors are " + + "different"); + do_check_neq(obs11, obs21); + + releaseLayoutChangesObserver(tabActor1); + releaseLayoutChangesObserver(tabActor1); + releaseLayoutChangesObserver(tabActor2); + releaseLayoutChangesObserver(tabActor2); +} + +function eventsAreBatched() { + do_print("Checking that reflow events are batched and only sent when the " + + "timeout expires"); + + // Note that in this test, we mock the TabActor and its window property, so we + // also mock the setTimeout/clearTimeout mechanism and just call the callback + // manually + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + + let reflowsEvents = []; + let onReflows = (event, reflows) => reflowsEvents.push(reflows); + observer.on("reflows", onReflows); + + let resizeEvents = []; + let onResize = () => resizeEvents.push("resize"); + observer.on("resize", onResize); + + do_print("Fake one reflow event"); + tabActor.window.docShell.observer.reflow(); + do_print("Checking that no batched reflow event has been emitted"); + do_check_eq(reflowsEvents.length, 0); + + do_print("Fake another reflow event"); + tabActor.window.docShell.observer.reflow(); + do_print("Checking that still no batched reflow event has been emitted"); + do_check_eq(reflowsEvents.length, 0); + + do_print("Fake a few of resize events too"); + tabActor.window.docShell.mockResize(); + tabActor.window.docShell.mockResize(); + tabActor.window.docShell.mockResize(); + do_print("Checking that still no batched resize event has been emitted"); + do_check_eq(resizeEvents.length, 0); + + do_print("Faking timeout expiration and checking that events are sent"); + observer.eventLoopTimer(); + do_check_eq(reflowsEvents.length, 1); + do_check_eq(reflowsEvents[0].length, 2); + do_check_eq(resizeEvents.length, 1); + + observer.off("reflows", onReflows); + observer.off("resize", onResize); + releaseLayoutChangesObserver(tabActor); +} + +function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() { + do_print("Checking that if no reflows were detected and the event batching " + + "loop expires, then no reflows event is sent"); + + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + + let reflowsEvents = []; + let onReflows = (event, reflows) => reflowsEvents.push(reflows); + observer.on("reflows", onReflows); + + do_print("Faking timeout expiration and checking for reflows"); + observer.eventLoopTimer(); + do_check_eq(reflowsEvents.length, 0); + + observer.off("reflows", onReflows); + releaseLayoutChangesObserver(tabActor); +} + +function observerIsAlreadyStarted() { + do_print("Checking that the observer is already started when getting it"); + + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + do_check_true(observer.isObserving); + + observer.stop(); + do_check_false(observer.isObserving); + + observer.start(); + do_check_true(observer.isObserving); + + releaseLayoutChangesObserver(tabActor); +} + +function destroyStopsObserving() { + do_print("Checking that the destroying the observer stops it"); + + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + do_check_true(observer.isObserving); + + observer.destroy(); + do_check_false(observer.isObserving); + + releaseLayoutChangesObserver(tabActor); +} + +function stoppingAndStartingSeveralTimesWorksCorrectly() { + do_print("Checking that the stopping and starting several times the observer" + + " works correctly"); + + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + + do_check_true(observer.isObserving); + observer.start(); + observer.start(); + observer.start(); + do_check_true(observer.isObserving); + + observer.stop(); + do_check_false(observer.isObserving); + + observer.stop(); + observer.stop(); + do_check_false(observer.isObserving); + + releaseLayoutChangesObserver(tabActor); +} + +function reflowsArentStackedWhenStopped() { + do_print("Checking that when stopped, reflows aren't stacked in the observer"); + + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + + do_print("Stoping the observer"); + observer.stop(); + + do_print("Faking reflows"); + tabActor.window.docShell.observer.reflow(); + tabActor.window.docShell.observer.reflow(); + tabActor.window.docShell.observer.reflow(); + + do_print("Checking that reflows aren't recorded"); + do_check_eq(observer.reflows.length, 0); + + do_print("Starting the observer and faking more reflows"); + observer.start(); + tabActor.window.docShell.observer.reflow(); + tabActor.window.docShell.observer.reflow(); + tabActor.window.docShell.observer.reflow(); + + do_print("Checking that reflows are recorded"); + do_check_eq(observer.reflows.length, 3); + + releaseLayoutChangesObserver(tabActor); +} + +function stackedReflowsAreResetOnStop() { + do_print("Checking that stacked reflows are reset on stop"); + + let tabActor = new MockTabActor(); + let observer = getLayoutChangesObserver(tabActor); + + tabActor.window.docShell.observer.reflow(); + do_check_eq(observer.reflows.length, 1); + + observer.stop(); + do_check_eq(observer.reflows.length, 0); + + tabActor.window.docShell.observer.reflow(); + do_check_eq(observer.reflows.length, 0); + + observer.start(); + do_check_eq(observer.reflows.length, 0); + + tabActor.window.docShell.observer.reflow(); + do_check_eq(observer.reflows.length, 1); + + releaseLayoutChangesObserver(tabActor); +} diff --git a/devtools/server/tests/unit/test_listsources-01.js b/devtools/server/tests/unit/test_listsources-01.js new file mode 100644 index 000000000..231e6a1e4 --- /dev/null +++ b/devtools/server/tests/unit/test_listsources-01.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic getSources functionality. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +var gNumTimesSourcesSent = 0; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.request = (function (request) { + return function (aRequest, aOnResponse) { + if (aRequest.type === "sources") { + ++gNumTimesSourcesSent; + } + return request.call(this, aRequest, aOnResponse); + }; + }(gClient.request)); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_listsources(); + }); + }); + do_test_pending(); +} + +function test_simple_listsources() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.getSources(function (aResponse) { + do_check_true(aResponse.sources.some(function (s) { + return s.url && s.url.match(/test_listsources-01.js/); + })); + + do_check_true(gNumTimesSourcesSent <= 1, + "Should only send one sources request at most, even though we" + + " might have had to send one to determine feature support."); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + Components.utils.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + gDebuggee); +} diff --git a/devtools/server/tests/unit/test_listsources-02.js b/devtools/server/tests/unit/test_listsources-02.js new file mode 100644 index 000000000..190a5e31b --- /dev/null +++ b/devtools/server/tests/unit/test_listsources-02.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check getting sources before there are any. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +var gNumTimesSourcesSent = 0; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.request = (function (request) { + return function (aRequest, aOnResponse) { + if (aRequest.type === "sources") { + ++gNumTimesSourcesSent; + } + return request.call(this, aRequest, aOnResponse); + }; + }(gClient.request)); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_listing_zero_sources(); + }); + }); + do_test_pending(); +} + +function test_listing_zero_sources() +{ + gThreadClient.getSources(function (aPacket) { + do_check_true(!aPacket.error); + do_check_true(!!aPacket.sources); + do_check_eq(aPacket.sources.length, 0); + + do_check_true(gNumTimesSourcesSent <= 1, + "Should only send one sources request at most, even though we" + + " might have had to send one to determine feature support."); + + finishClient(gClient); + }); +} diff --git a/devtools/server/tests/unit/test_listsources-03.js b/devtools/server/tests/unit/test_listsources-03.js new file mode 100644 index 000000000..72ebb5e1c --- /dev/null +++ b/devtools/server/tests/unit/test_listsources-03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check getSources functionality when there are lots of sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-sources"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-sources", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_listsources(); + }); + }); + do_test_pending(); +} + +function test_simple_listsources() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.getSources(function (aResponse) { + do_check_true( + !aResponse.error, + "There shouldn't be an error fetching large amounts of sources."); + + do_check_true(aResponse.sources.some(function (s) { + return s.url.match(/foo-999.js$/); + })); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + for (let i = 0; i < 1000; i++) { + Cu.evalInSandbox("function foo###() {return ###;}".replace(/###/g, i), + gDebuggee, + "1.8", + "http://example.com/foo-" + i + ".js", + 1); + } + gDebuggee.eval("debugger;"); +} diff --git a/devtools/server/tests/unit/test_listsources-04.js b/devtools/server/tests/unit/test_listsources-04.js new file mode 100644 index 000000000..6da99a6ce --- /dev/null +++ b/devtools/server/tests/unit/test_listsources-04.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check getSources functionality with sourcemaps. + */ + +const {SourceNode} = require("source-map"); + +function run_test() { + run_test_with_server(DebuggerServer, function () { + // Bug 1304144 - This test does not run in a worker because the + // `rpc` method which talks to the main thread does not work. + // run_test_with_server(WorkerDebuggerServer, do_test_finished); + do_test_finished(); + }); + do_test_pending(); +} + +function run_test_with_server(server, cb) { + Task.spawn(function*() { + initTestDebuggerServer(server); + const debuggee = addTestGlobal("test-sources", server); + const client = new DebuggerClient(server.connectPipe()); + yield client.connect(); + const [,,threadClient] = yield attachTestTabAndResume(client, "test-sources"); + + yield threadClient.reconfigure({ useSourceMaps: true }); + addSources(debuggee); + + threadClient.getSources(Task.async(function* (res) { + do_check_true(res.sources.length === 3, "3 sources exist"); + + yield threadClient.reconfigure({ useSourceMaps: false }); + + threadClient.getSources(function(res) { + do_check_true(res.sources.length === 1, "1 source exist"); + client.close().then(cb); + }); + })); + }); +} + +function addSources(debuggee) { + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"), + new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"), + new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, debuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/devtools/server/tests/unit/test_longstringactor.js b/devtools/server/tests/unit/test_longstringactor.js new file mode 100644 index 000000000..18b928910 --- /dev/null +++ b/devtools/server/tests/unit/test_longstringactor.js @@ -0,0 +1,104 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { LongStringActor } = require("devtools/server/actors/object"); + +function run_test() { + test_LSA_disconnect(); + test_LSA_grip(); + test_LSA_onSubstring(); +} + +const TEST_STRING = "This is a very long string!"; + +function makeMockLongStringActor() +{ + let string = TEST_STRING; + let actor = new LongStringActor(string); + actor.actorID = "longString1"; + actor.registeredPool = { + longStringActors: { + [string]: actor + } + }; + return actor; +} + +function test_LSA_disconnect() +{ + let actor = makeMockLongStringActor(); + do_check_eq(actor.registeredPool.longStringActors[TEST_STRING], actor); + + actor.disconnect(); + do_check_eq(actor.registeredPool.longStringActors[TEST_STRING], void 0); +} + +function test_LSA_substring() +{ + let actor = makeMockLongStringActor(); + do_check_eq(actor._substring(0, 4), TEST_STRING.substring(0, 4)); + do_check_eq(actor._substring(6, 9), TEST_STRING.substring(6, 9)); + do_check_eq(actor._substring(0, TEST_STRING.length), TEST_STRING); +} + +function test_LSA_grip() +{ + let actor = makeMockLongStringActor(); + + let grip = actor.grip(); + do_check_eq(grip.type, "longString"); + do_check_eq(grip.initial, TEST_STRING.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH)); + do_check_eq(grip.length, TEST_STRING.length); + do_check_eq(grip.actor, actor.actorID); +} + +function test_LSA_onSubstring() +{ + let actor = makeMockLongStringActor(); + let response; + + // From the start + response = actor.onSubstring({ + start: 0, + end: 4 + }); + do_check_eq(response.from, actor.actorID); + do_check_eq(response.substring, TEST_STRING.substring(0, 4)); + + // In the middle + response = actor.onSubstring({ + start: 5, + end: 8 + }); + do_check_eq(response.from, actor.actorID); + do_check_eq(response.substring, TEST_STRING.substring(5, 8)); + + // Whole string + response = actor.onSubstring({ + start: 0, + end: TEST_STRING.length + }); + do_check_eq(response.from, actor.actorID); + do_check_eq(response.substring, TEST_STRING); + + // Negative index + response = actor.onSubstring({ + start: -5, + end: TEST_STRING.length + }); + do_check_eq(response.from, actor.actorID); + do_check_eq(response.substring, + TEST_STRING.substring(-5, TEST_STRING.length)); + + // Past the end + response = actor.onSubstring({ + start: TEST_STRING.length - 5, + end: 100 + }); + do_check_eq(response.from, actor.actorID); + do_check_eq(response.substring, + TEST_STRING.substring(TEST_STRING.length - 5, 100)); +} diff --git a/devtools/server/tests/unit/test_longstringgrips-01.js b/devtools/server/tests/unit/test_longstringgrips-01.js new file mode 100644 index 000000000..b8e6789c7 --- /dev/null +++ b/devtools/server/tests/unit/test_longstringgrips-01.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_longstring_grip(); + }); + }); + do_test_pending(); +} + +function test_longstring_grip() +{ + let longString = "All I want is to be a monkey of moderate intelligence who" + + " wears a suit... that's why I'm transferring to business school! Maybe I" + + " love you so much, I love you no matter who you are pretending to be." + + " Enough about your promiscuous mother, Hermes! We have bigger problems." + + " For example, if you killed your grandfather, you'd cease to exist! What" + + " kind of a father would I be if I said no? Yep, I remember. They came in" + + " last at the Olympics, then retired to promote alcoholic beverages! And" + + " remember, don't do anything that affects anything, unless it turns out" + + " you were supposed to, in which case, for the love of God, don't not do" + + " it!"; + + DebuggerServer.LONG_STRING_LENGTH = 200; + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + do_check_eq(args.length, 1); + let grip = args[0]; + + try { + do_check_eq(grip.type, "longString"); + do_check_eq(grip.length, longString.length); + do_check_eq(grip.initial, longString.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH)); + + let longStringClient = gThreadClient.pauseLongString(grip); + longStringClient.substring(22, 28, function (aResponse) { + try { + do_check_eq(aResponse.substring, "monkey"); + } finally { + gThreadClient.resume(function () { + finishClient(gClient); + }); + } + }); + } catch (error) { + gThreadClient.resume(function () { + finishClient(gClient); + do_throw(error); + }); + } + }); + + gDebuggee.eval('stopMe("' + longString + '")'); +} + diff --git a/devtools/server/tests/unit/test_longstringgrips-02.js b/devtools/server/tests/unit/test_longstringgrips-02.js new file mode 100644 index 000000000..01f9c1b8f --- /dev/null +++ b/devtools/server/tests/unit/test_longstringgrips-02.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume( + gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_longstring_grip(); + }); + }); + do_test_pending(); +} + +function test_longstring_grip() +{ + DebuggerServer.LONG_STRING_LENGTH = 200; + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + try { + let fakeLongStringGrip = { + type: "longString", + length: 1000000, + actor: "123fakeActor123", + initial: "" + }; + let longStringClient = gThreadClient.pauseLongString(fakeLongStringGrip); + longStringClient.substring(22, 28, function (aResponse) { + try { + do_check_true(!!aResponse.error, + "We should not get a response, but an error."); + } finally { + gThreadClient.resume(function () { + finishClient(gClient); + }); + } + }); + } catch (error) { + gThreadClient.resume(function () { + finishClient(gClient); + do_throw(error); + }); + } + }); + + gDebuggee.eval("stopMe()"); +} + diff --git a/devtools/server/tests/unit/test_monitor_actor.js b/devtools/server/tests/unit/test_monitor_actor.js new file mode 100644 index 000000000..17c272d80 --- /dev/null +++ b/devtools/server/tests/unit/test_monitor_actor.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test the monitor actor. + */ + +"use strict"; + +function run_test() +{ + let EventEmitter = require("devtools/shared/event-emitter"); + + function MonitorClient(client, form) { + this.client = client; + this.actor = form.monitorActor; + this.events = ["update"]; + + EventEmitter.decorate(this); + client.registerClient(this); + } + MonitorClient.prototype.destroy = function () { + this.client.unregisterClient(this); + }; + MonitorClient.prototype.start = function (callback) { + this.client.request({ + to: this.actor, + type: "start" + }, callback); + }; + MonitorClient.prototype.stop = function (callback) { + this.client.request({ + to: this.actor, + type: "stop" + }, callback); + }; + + let monitor, client; + + // Start the monitor actor. + get_chrome_actors((c, form) => { + client = c; + monitor = new MonitorClient(client, form); + monitor.on("update", gotUpdate); + monitor.start(update); + }); + + let time = Date.now(); + + function update() { + let event = { + graph: "Test", + curve: "test", + value: 42, + time: time, + }; + Services.obs.notifyObservers(null, "devtools-monitor-update", JSON.stringify(event)); + } + + function gotUpdate(type, packet) { + packet.data.forEach(function (event) { + // Ignore updates that were not sent by this test. + if (event.graph === "Test") { + do_check_eq(event.curve, "test"); + do_check_eq(event.value, 42); + do_check_eq(event.time, time); + monitor.stop(function (aResponse) { + monitor.destroy(); + finishClient(client); + }); + } + }); + } + + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_nativewrappers.js b/devtools/server/tests/unit/test_nativewrappers.js new file mode 100644 index 000000000..fbadfcdec --- /dev/null +++ b/devtools/server/tests/unit/test_nativewrappers.js @@ -0,0 +1,30 @@ +function run_test() +{ + Components.utils.import("resource://gre/modules/jsdebugger.jsm"); + addDebuggerToGlobal(this); + var g = testGlobal("test1"); + + var dbg = new Debugger(); + dbg.addDebuggee(g); + dbg.onDebuggerStatement = function (aFrame) { + let args = aFrame.arguments; + try { + args[0]; + do_check_true(true); + } catch (ex) { + do_check_true(false); + } + }; + + g.eval("function stopMe(arg) {debugger;}"); + + g2 = testGlobal("test2"); + g2.g = g; + g2.eval("(" + function createBadEvent() { + let parser = Components.classes["@mozilla.org/xmlextras/domparser;1"].createInstance(Components.interfaces.nsIDOMParser); + let doc = parser.parseFromString("", "text/xml"); + g.stopMe(doc.createEvent("MouseEvent")); + } + ")()"); + + dbg.enabled = false; +} diff --git a/devtools/server/tests/unit/test_nesting-01.js b/devtools/server/tests/unit/test_nesting-01.js new file mode 100644 index 000000000..e515f051e --- /dev/null +++ b/devtools/server/tests/unit/test_nesting-01.js @@ -0,0 +1,48 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can nest event loops when needed in +// ThreadActor.prototype.unsafeSynchronize. + +var gClient; +var gThreadActor; + +function run_test() { + initTestDebuggerServer(); + let gDebuggee = addTestGlobal("test-nesting"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-nesting", function (aResponse, aTabClient, aThreadClient) { + // Reach over the protocol connection and get a reference to the thread actor. + gThreadActor = aThreadClient._transport._serverConnection.getActor(aThreadClient._actor); + + test_nesting(); + }); + }); + do_test_pending(); +} + +function test_nesting() { + const thread = gThreadActor; + const { resolve, reject, promise: p } = promise.defer(); + + let currentStep = 0; + + executeSoon(function () { + // Should be on the first step + do_check_eq(++currentStep, 1); + // We should have one nested event loop from unsfeSynchronize + do_check_eq(thread._nestedEventLoops.size, 1); + resolve(true); + }); + + do_check_eq(thread.unsafeSynchronize(p), true); + + // Should be on the second step + do_check_eq(++currentStep, 2); + // There shouldn't be any nested event loops anymore + do_check_eq(thread._nestedEventLoops.size, 0); + + finishClient(gClient); +} diff --git a/devtools/server/tests/unit/test_nesting-02.js b/devtools/server/tests/unit/test_nesting-02.js new file mode 100644 index 000000000..928331be5 --- /dev/null +++ b/devtools/server/tests/unit/test_nesting-02.js @@ -0,0 +1,81 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can nest event loops and then automatically exit nested event +// loops when requested. + +var gClient; +var gThreadActor; + +function run_test() { + initTestDebuggerServer(); + let gDebuggee = addTestGlobal("test-nesting"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-nesting", function (aResponse, aTabClient, aThreadClient) { + // Reach over the protocol connection and get a reference to the thread + // actor. + gThreadActor = aThreadClient._transport._serverConnection.getActor(aThreadClient._actor); + + test_nesting(); + }); + }); + do_test_pending(); +} + +function test_nesting() { + const thread = gThreadActor; + const { resolve, reject, promise: p } = promise.defer(); + + // The following things should happen (in order): + // 1. In the new event loop (created by unsafeSynchronize) + // 2. Resolve the promise (shouldn't exit any event loops) + // 3. Exit the event loop (should also then exit unsafeSynchronize's event loop) + // 4. Be after the unsafeSynchronize call + let currentStep = 0; + + executeSoon(function () { + let eventLoop; + + executeSoon(function () { + // Should be at step 2 + do_check_eq(++currentStep, 2); + // Before resolving, should have the unsafeSynchronize event loop and the + // one just created. + do_check_eq(thread._nestedEventLoops.size, 2); + + executeSoon(function () { + // Should be at step 3 + do_check_eq(++currentStep, 3); + // Before exiting the manually created event loop, should have the + // unsafeSynchronize event loop and the manual event loop. + do_check_eq(thread._nestedEventLoops.size, 2); + // Should have the event loop + do_check_true(!!eventLoop); + eventLoop.resolve(); + }); + + resolve(true); + // Shouldn't exit any event loops because a new one started since the call + // to unsafeSynchronize + do_check_eq(thread._nestedEventLoops.size, 2); + }); + + // Should be at step 1 + do_check_eq(++currentStep, 1); + // Should have only the unsafeSynchronize event loop + do_check_eq(thread._nestedEventLoops.size, 1); + eventLoop = thread._nestedEventLoops.push(); + eventLoop.enter(); + }); + + do_check_eq(thread.unsafeSynchronize(p), true); + + // Should be on the fourth step + do_check_eq(++currentStep, 4); + // There shouldn't be any nested event loops anymore + do_check_eq(thread._nestedEventLoops.size, 0); + + finishClient(gClient); +} diff --git a/devtools/server/tests/unit/test_nesting-03.js b/devtools/server/tests/unit/test_nesting-03.js new file mode 100644 index 000000000..6a0e5a66b --- /dev/null +++ b/devtools/server/tests/unit/test_nesting-03.js @@ -0,0 +1,51 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can detect nested event loops in tabs with the same URL. + +var gClient1, gClient2, gThreadClient1, gThreadClient2; + +function run_test() { + initTestDebuggerServer(); + addTestGlobal("test-nesting1"); + addTestGlobal("test-nesting1"); + // Conect the first client to the first debuggee. + gClient1 = new DebuggerClient(DebuggerServer.connectPipe()); + gClient1.connect(function () { + attachTestThread(gClient1, "test-nesting1", function (aResponse, aTabClient, aThreadClient) { + gThreadClient1 = aThreadClient; + start_second_connection(); + }); + }); + do_test_pending(); +} + +function start_second_connection() { + gClient2 = new DebuggerClient(DebuggerServer.connectPipe()); + gClient2.connect(function () { + attachTestThread(gClient2, "test-nesting1", function (aResponse, aTabClient, aThreadClient) { + gThreadClient2 = aThreadClient; + test_nesting(); + }); + }); +} + +function test_nesting() { + const { resolve, reject, promise: p } = promise.defer(); + + gThreadClient1.resume(aResponse => { + do_check_eq(aResponse.error, "wrongOrder"); + gThreadClient2.resume(aResponse => { + do_check_true(!aResponse.error); + do_check_eq(aResponse.from, gThreadClient2.actor); + + gThreadClient1.resume(aResponse => { + do_check_true(!aResponse.error); + do_check_eq(aResponse.from, gThreadClient1.actor); + + gClient1.close(() => finishClient(gClient2)); + }); + }); + }); +} diff --git a/devtools/server/tests/unit/test_new_source-01.js b/devtools/server/tests/unit/test_new_source-01.js new file mode 100644 index 000000000..aa2498371 --- /dev/null +++ b/devtools/server/tests/unit/test_new_source-01.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic newSource packet sent from server. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_new_source(); + }); + }); + do_test_pending(); +} + +function test_simple_new_source() +{ + gThreadClient.addOneTimeListener("newSource", function (aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + do_check_true(!!aPacket.source.url.match(/test_new_source-01.js$/)); + + finishClient(gClient); + }); + + Components.utils.evalInSandbox(function inc(n) { + return n + 1; + }.toString(), gDebuggee); +} diff --git a/devtools/server/tests/unit/test_nodelistactor.js b/devtools/server/tests/unit/test_nodelistactor.js new file mode 100644 index 000000000..4d9ec1a7a --- /dev/null +++ b/devtools/server/tests/unit/test_nodelistactor.js @@ -0,0 +1,26 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that a NodeListActor initialized with null nodelist doesn't cause +// exceptions when calling NodeListActor.form. + +const { NodeListActor } = require("devtools/server/actors/inspector"); + +function run_test() { + check_actor_for_list(null); + check_actor_for_list([]); + check_actor_for_list(["fakenode"]); +} + +function check_actor_for_list(nodelist) { + do_print("Checking NodeListActor with nodelist '" + nodelist + "' works."); + let actor = new NodeListActor({}, nodelist); + let form = actor.form(); + + // No exception occured as a exceptions abort the test. + ok(true, "No exceptions occured."); + equal(form.length, nodelist ? nodelist.length : 0, + "NodeListActor reported correct length."); +} diff --git a/devtools/server/tests/unit/test_nsjsinspector.js b/devtools/server/tests/unit/test_nsjsinspector.js new file mode 100644 index 000000000..14a99a308 --- /dev/null +++ b/devtools/server/tests/unit/test_nsjsinspector.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the basic functionality of the nsIJSInspector component. +var gCount = 0; +const MAX = 10; +var inspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); +var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + +// Emulate 10 simultaneously-debugged windows from 3 separate client connections. +var requestor = (count) => ({ + url:"http://foo/bar/" + count, + connection: "conn" + (count % 3) +}); + +function run_test() +{ + test_nesting(); +} + +function test_nesting() +{ + do_check_eq(inspector.eventLoopNestLevel, 0); + + tm.currentThread.dispatch({ run: enterEventLoop}, 0); + + do_check_eq(inspector.enterNestedEventLoop(requestor(gCount)), 0); + do_check_eq(inspector.eventLoopNestLevel, 0); + do_check_eq(inspector.lastNestRequestor, null); +} + +function enterEventLoop() { + if (gCount++ < MAX) { + tm.currentThread.dispatch({ run: enterEventLoop}, 0); + + let r = Object.create(requestor(gCount)); + + do_check_eq(inspector.eventLoopNestLevel, gCount); + do_check_eq(inspector.lastNestRequestor.url, requestor(gCount - 1).url); + do_check_eq(inspector.lastNestRequestor.connection, requestor(gCount - 1).connection); + do_check_eq(inspector.enterNestedEventLoop(requestor(gCount)), gCount); + } else { + do_check_eq(gCount, MAX + 1); + tm.currentThread.dispatch({ run: exitEventLoop}, 0); + } +} + +function exitEventLoop() { + if (inspector.lastNestRequestor != null) { + do_check_eq(inspector.lastNestRequestor.url, requestor(gCount - 1).url); + do_check_eq(inspector.lastNestRequestor.connection, requestor(gCount - 1).connection); + if (gCount-- > 1) { + tm.currentThread.dispatch({ run: exitEventLoop}, 0); + } + + do_check_eq(inspector.exitNestedEventLoop(), gCount); + do_check_eq(inspector.eventLoopNestLevel, gCount); + } +} diff --git a/devtools/server/tests/unit/test_objectgrips-01.js b/devtools/server/tests/unit/test_objectgrips-01.js new file mode 100644 index 000000000..e1857e5b8 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Object"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getOwnPropertyNames(function (aResponse) { + do_check_eq(aResponse.ownPropertyNames.length, 3); + do_check_eq(aResponse.ownPropertyNames[0], "a"); + do_check_eq(aResponse.ownPropertyNames[1], "b"); + do_check_eq(aResponse.ownPropertyNames[2], "c"); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + + }); + + gDebuggee.eval("stopMe({ a: 1, b: true, c: 'foo' })"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-02.js b/devtools/server/tests/unit/test_objectgrips-02.js new file mode 100644 index 000000000..649d52c64 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-02.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Object"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getPrototype(function (aResponse) { + do_check_true(aResponse.prototype != undefined); + + let protoClient = gThreadClient.pauseGrip(aResponse.prototype); + protoClient.getOwnPropertyNames(function (aResponse) { + do_check_eq(aResponse.ownPropertyNames.length, 2); + do_check_eq(aResponse.ownPropertyNames[0], "b"); + do_check_eq(aResponse.ownPropertyNames[1], "c"); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + }); + + gDebuggee.eval(function Constr() { + this.a = 1; + }.toString()); + gDebuggee.eval("Constr.prototype = { b: true, c: 'foo' }; var o = new Constr(); stopMe(o)"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-03.js b/devtools/server/tests/unit/test_objectgrips-03.js new file mode 100644 index 000000000..8b19db713 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-03.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Object"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getProperty("x", function (aResponse) { + do_check_eq(aResponse.descriptor.configurable, true); + do_check_eq(aResponse.descriptor.enumerable, true); + do_check_eq(aResponse.descriptor.writable, true); + do_check_eq(aResponse.descriptor.value, 10); + + objClient.getProperty("y", function (aResponse) { + do_check_eq(aResponse.descriptor.configurable, true); + do_check_eq(aResponse.descriptor.enumerable, true); + do_check_eq(aResponse.descriptor.writable, true); + do_check_eq(aResponse.descriptor.value, "kaiju"); + + objClient.getProperty("a", function (aResponse) { + do_check_eq(aResponse.descriptor.configurable, true); + do_check_eq(aResponse.descriptor.enumerable, true); + do_check_eq(aResponse.descriptor.get.type, "object"); + do_check_eq(aResponse.descriptor.get.class, "Function"); + do_check_eq(aResponse.descriptor.set.type, "undefined"); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + }); + + }); + + gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-04.js b/devtools/server/tests/unit/test_objectgrips-04.js new file mode 100644 index 000000000..1662358e0 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-04.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Object"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.x.configurable, true); + do_check_eq(aResponse.ownProperties.x.enumerable, true); + do_check_eq(aResponse.ownProperties.x.writable, true); + do_check_eq(aResponse.ownProperties.x.value, 10); + + do_check_eq(aResponse.ownProperties.y.configurable, true); + do_check_eq(aResponse.ownProperties.y.enumerable, true); + do_check_eq(aResponse.ownProperties.y.writable, true); + do_check_eq(aResponse.ownProperties.y.value, "kaiju"); + + do_check_eq(aResponse.ownProperties.a.configurable, true); + do_check_eq(aResponse.ownProperties.a.enumerable, true); + do_check_eq(aResponse.ownProperties.a.get.type, "object"); + do_check_eq(aResponse.ownProperties.a.get.class, "Function"); + do_check_eq(aResponse.ownProperties.a.set.type, "undefined"); + + do_check_true(aResponse.prototype != undefined); + + let protoClient = gThreadClient.pauseGrip(aResponse.prototype); + protoClient.getOwnPropertyNames(function (aResponse) { + do_check_true(aResponse.ownPropertyNames.toString != undefined); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + }); + + gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-05.js b/devtools/server/tests/unit/test_objectgrips-05.js new file mode 100644 index 000000000..5bbb37d88 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-05.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks that frozen objects report themselves as frozen in their + * grip. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1, arg2) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let obj1 = aPacket.frame.arguments[0]; + do_check_true(obj1.frozen); + + let obj1Client = gThreadClient.pauseGrip(obj1); + do_check_true(obj1Client.isFrozen); + + let obj2 = aPacket.frame.arguments[1]; + do_check_false(obj2.frozen); + + let obj2Client = gThreadClient.pauseGrip(obj2); + do_check_false(obj2Client.isFrozen); + + gThreadClient.resume(_ => { + gClient.close().then(gCallback); + }); + }); + + gDebuggee.eval("(" + function () { + let obj1 = {}; + Object.freeze(obj1); + stopMe(obj1, {}); + } + "())"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-06.js b/devtools/server/tests/unit/test_objectgrips-06.js new file mode 100644 index 000000000..bb9888ab8 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-06.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks that sealed objects report themselves as sealed in their + * grip. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1, arg2) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let obj1 = aPacket.frame.arguments[0]; + do_check_true(obj1.sealed); + + let obj1Client = gThreadClient.pauseGrip(obj1); + do_check_true(obj1Client.isSealed); + + let obj2 = aPacket.frame.arguments[1]; + do_check_false(obj2.sealed); + + let obj2Client = gThreadClient.pauseGrip(obj2); + do_check_false(obj2Client.isSealed); + + gThreadClient.resume(_ => { + gClient.close().then(gCallback); + }); + }); + + gDebuggee.eval("(" + function () { + let obj1 = {}; + Object.seal(obj1); + stopMe(obj1, {}); + } + "())"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-07.js b/devtools/server/tests/unit/test_objectgrips-07.js new file mode 100644 index 000000000..6d9ac11fb --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-07.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks that objects which are not extensible report themselves as + * such. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1, arg2, arg3, arg4) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let [f, s, ne, e] = aPacket.frame.arguments; + let [fClient, sClient, neClient, eClient] = aPacket.frame.arguments.map( + a => gThreadClient.pauseGrip(a)); + + do_check_false(f.extensible); + do_check_false(fClient.isExtensible); + + do_check_false(s.extensible); + do_check_false(sClient.isExtensible); + + do_check_false(ne.extensible); + do_check_false(neClient.isExtensible); + + do_check_true(e.extensible); + do_check_true(eClient.isExtensible); + + gThreadClient.resume(_ => { + gClient.close().then(gCallback); + }); + }); + + gDebuggee.eval("(" + function () { + let f = {}; + Object.freeze(f); + let s = {}; + Object.seal(s); + let ne = {}; + Object.preventExtensions(ne); + stopMe(f, s, ne, {}); + } + "())"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-08.js b/devtools/server/tests/unit/test_objectgrips-08.js new file mode 100644 index 000000000..ecaa7146d --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-08.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + do_check_eq(args[0].class, "Object"); + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getPrototypeAndProperties(function (aResponse) { + do_check_eq(aResponse.ownProperties.a.configurable, true); + do_check_eq(aResponse.ownProperties.a.enumerable, true); + do_check_eq(aResponse.ownProperties.a.writable, true); + do_check_eq(aResponse.ownProperties.a.value.type, "Infinity"); + + do_check_eq(aResponse.ownProperties.b.configurable, true); + do_check_eq(aResponse.ownProperties.b.enumerable, true); + do_check_eq(aResponse.ownProperties.b.writable, true); + do_check_eq(aResponse.ownProperties.b.value.type, "-Infinity"); + + do_check_eq(aResponse.ownProperties.c.configurable, true); + do_check_eq(aResponse.ownProperties.c.enumerable, true); + do_check_eq(aResponse.ownProperties.c.writable, true); + do_check_eq(aResponse.ownProperties.c.value.type, "NaN"); + + do_check_eq(aResponse.ownProperties.d.configurable, true); + do_check_eq(aResponse.ownProperties.d.enumerable, true); + do_check_eq(aResponse.ownProperties.d.writable, true); + do_check_eq(aResponse.ownProperties.d.value.type, "-0"); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + }); + + gDebuggee.eval("stopMe({ a: Infinity, b: -Infinity, c: NaN, d: -0 })"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-09.js b/devtools/server/tests/unit/test_objectgrips-09.js new file mode 100644 index 000000000..498154b1e --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-09.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/** + * This tests exercises getProtypesAndProperties message accepted + * by a thread actor. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-grips", aServer); + gDebuggee.eval(function stopMe(arg1, arg2) { + debugger; + }.toString()); + + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + gThreadClient.getPrototypesAndProperties([args[0].actor, args[1].actor], function (aResponse) { + let obj1 = aResponse.actors[args[0].actor]; + let obj2 = aResponse.actors[args[1].actor]; + do_check_eq(obj1.ownProperties.x.configurable, true); + do_check_eq(obj1.ownProperties.x.enumerable, true); + do_check_eq(obj1.ownProperties.x.writable, true); + do_check_eq(obj1.ownProperties.x.value, 10); + + do_check_eq(obj1.ownProperties.y.configurable, true); + do_check_eq(obj1.ownProperties.y.enumerable, true); + do_check_eq(obj1.ownProperties.y.writable, true); + do_check_eq(obj1.ownProperties.y.value, "kaiju"); + + do_check_eq(obj2.ownProperties.z.configurable, true); + do_check_eq(obj2.ownProperties.z.enumerable, true); + do_check_eq(obj2.ownProperties.z.writable, true); + do_check_eq(obj2.ownProperties.z.value, 123); + + do_check_true(obj1.prototype != undefined); + do_check_true(obj2.prototype != undefined); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + + }); + + gDebuggee.eval("stopMe({ x: 10, y: 'kaiju'}, { z: 123 })"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-10.js b/devtools/server/tests/unit/test_objectgrips-10.js new file mode 100644 index 000000000..a5d1b18c6 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-10.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +// Test that closures can be inspected. + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-closures"); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-closures", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); + do_test_pending(); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let person = aPacket.frame.environment.bindings.variables.person; + + do_check_eq(person.value.class, "Object"); + + let personClient = gThreadClient.pauseGrip(person.value); + personClient.getPrototypeAndProperties(aResponse => { + do_check_eq(aResponse.ownProperties.getName.value.class, "Function"); + + do_check_eq(aResponse.ownProperties.getAge.value.class, "Function"); + + do_check_eq(aResponse.ownProperties.getFoo.value.class, "Function"); + + let getNameClient = gThreadClient.pauseGrip(aResponse.ownProperties.getName.value); + let getAgeClient = gThreadClient.pauseGrip(aResponse.ownProperties.getAge.value); + let getFooClient = gThreadClient.pauseGrip(aResponse.ownProperties.getFoo.value); + getNameClient.getScope(aResponse => { + do_check_eq(aResponse.scope.bindings.arguments[0].name.value, "Bob"); + + getAgeClient.getScope(aResponse => { + do_check_eq(aResponse.scope.bindings.arguments[1].age.value, 58); + + getFooClient.getScope(aResponse => { + do_check_eq(aResponse.scope.bindings.variables.foo.value, 10); + + gThreadClient.resume(() => finishClient(gClient)); + }); + }); + }); + }); + + }); + + gDebuggee.eval("(" + function () { + var PersonFactory = function (name, age) { + var foo = 10; + return { + getName: function () { return name; }, + getAge: function () { return age; }, + getFoo: function () { foo = Date.now(); return foo; } + }; + }; + var person = new PersonFactory("Bob", 58); + debugger; + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_objectgrips-11.js b/devtools/server/tests/unit/test_objectgrips-11.js new file mode 100644 index 000000000..1ad5c353a --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-11.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we get the magic properties on Error objects. + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_object_grip(); + }); + }); + do_test_pending(); +} + +function test_object_grip() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + + let objClient = gThreadClient.pauseGrip(args[0]); + objClient.getOwnPropertyNames(function (aResponse) { + var opn = aResponse.ownPropertyNames; + do_check_eq(opn.length, 4); + opn.sort(); + do_check_eq(opn[0], "columnNumber"); + do_check_eq(opn[1], "fileName"); + do_check_eq(opn[2], "lineNumber"); + do_check_eq(opn[3], "message"); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + }); + + gDebuggee.eval("stopMe(new TypeError('error message text'))"); +} + diff --git a/devtools/server/tests/unit/test_objectgrips-12.js b/devtools/server/tests/unit/test_objectgrips-12.js new file mode 100644 index 000000000..32d4d47e0 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-12.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test getDisplayString. + +Cu.import("resource://testing-common/PromiseTestUtils.jsm", this); + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gDebuggee.eval(function stopMe(arg1) { + debugger; + }.toString()); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_display_string(); + }); + }); + do_test_pending(); +} + +function test_display_string() +{ + const testCases = [ + { + input: "new Boolean(true)", + output: "true" + }, + { + input: "new Number(5)", + output: "5" + }, + { + input: "new String('foo')", + output: "foo" + }, + { + input: "new Map()", + output: "[object Map]" + }, + { + input: "[,,,,,,,]", + output: ",,,,,," + }, + { + input: "[1, 2, 3]", + output: "1,2,3" + }, + { + input: "[undefined, null, true, 'foo', 5]", + output: ",,true,foo,5" + }, + { + input: "[{},{}]", + output: "[object Object],[object Object]" + }, + { + input: "(" + function () { + const arr = [1]; + arr.push(arr); + return arr; + } + ")()", + output: "1," + }, + { + input: "{}", + output: "[object Object]" + }, + { + input: "Object.create(null)", + output: "[object Object]" + }, + { + input: "new Error('foo')", + output: "Error: foo" + }, + { + input: "new SyntaxError()", + output: "SyntaxError" + }, + { + input: "new ReferenceError('')", + output: "ReferenceError" + }, + { + input: "(" + function () { + const err = new Error("bar"); + err.name = "foo"; + return err; + } + ")()", + output: "foo: bar" + }, + { + input: "() => {}", + output: "() => {}" + }, + { + input: "function (foo, bar) {}", + output: "function (foo, bar) {}" + }, + { + input: "function foo(bar) {}", + output: "function foo(bar) {}" + }, + { + input: "Array", + output: Array + "" + }, + { + input: "/foo[bar]/g", + output: "/foo[bar]/g" + }, + { + input: "new Proxy({}, {})", + output: "[object Object]" + }, + { + input: "Promise.resolve(5)", + output: "Promise (fulfilled: 5)" + }, + { + // This rejection is left uncaught, see expectUncaughtRejection below. + input: "Promise.reject(new Error())", + output: "Promise (rejected: Error)" + }, + { + input: "new Promise(function () {})", + output: "Promise (pending)" + } + ]; + + PromiseTestUtils.expectUncaughtRejection(/Error/); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + const args = aPacket.frame.arguments; + + (function loop() { + const objClient = gThreadClient.pauseGrip(args.pop()); + objClient.getDisplayString(function ({ displayString }) { + do_check_eq(displayString, testCases.pop().output); + if (args.length) { + loop(); + } else { + gThreadClient.resume(function () { + finishClient(gClient); + }); + } + }); + })(); + }); + + const inputs = testCases.map(({ input }) => input).join(","); + gDebuggee.eval("stopMe(" + inputs + ")"); +} diff --git a/devtools/server/tests/unit/test_objectgrips-13.js b/devtools/server/tests/unit/test_objectgrips-13.js new file mode 100644 index 000000000..166e8a0d5 --- /dev/null +++ b/devtools/server/tests/unit/test_objectgrips-13.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that ObjectClient.prototype.getDefinitionSite and the "definitionSite" +// request work properly. + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + Components.utils.evalInSandbox(function stopMe() { + debugger; + }.toString(), gDebuggee); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + add_pause_listener(); + }); + }); + do_test_pending(); +} + +function add_pause_listener() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + const [funcGrip, objGrip] = aPacket.frame.arguments; + const func = gThreadClient.pauseGrip(funcGrip); + const obj = gThreadClient.pauseGrip(objGrip); + test_definition_site(func, obj); + }); + + eval_code(); +} + +function eval_code() { + Components.utils.evalInSandbox([ + "this.line0 = Error().lineNumber;", + "function f() {}", + "stopMe(f, {});" + ].join("\n"), gDebuggee); +} + +function test_definition_site(func, obj) { + func.getDefinitionSite(({ error, source, line, column }) => { + do_check_true(!error); + do_check_eq(source.url, getFilePath("test_objectgrips-13.js")); + do_check_eq(line, gDebuggee.line0 + 1); + do_check_eq(column, 0); + + test_bad_definition_site(obj); + }); +} + +function test_bad_definition_site(obj) { + try { + obj._client.request("definitionSite", () => do_check_true(false)); + } catch (e) { + gThreadClient.resume(() => finishClient(gClient)); + } +} diff --git a/devtools/server/tests/unit/test_pause_exceptions-01.js b/devtools/server/tests/unit/test_pause_exceptions-01.js new file mode 100644 index 000000000..56ee6816d --- /dev/null +++ b/devtools/server/tests/unit/test_pause_exceptions-01.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that setting pauseOnExceptions to true will cause the debuggee to pause + * when an exceptions is thrown. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "exception"); + do_check_eq(aPacket.why.exception, 42); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + gThreadClient.pauseOnExceptions(true); + gThreadClient.resume(); + }); + + gDebuggee.eval("(" + function () { + function stopMe() { + debugger; + throw 42; + } + try { + stopMe(); + } catch (e) {} + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_pause_exceptions-02.js b/devtools/server/tests/unit/test_pause_exceptions-02.js new file mode 100644 index 000000000..fa9b419f0 --- /dev/null +++ b/devtools/server/tests/unit/test_pause_exceptions-02.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that setting pauseOnExceptions to true when the debugger isn't in a + * paused state will cause the debuggee to pause when an exceptions is thrown. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.pauseOnExceptions(true, false, function () { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "exception"); + do_check_eq(aPacket.why.exception, 42); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe() { + throw 42; + } + try { + stopMe(); + } catch (e) {} + } + ")()"); + }); +} diff --git a/devtools/server/tests/unit/test_pauselifetime-01.js b/devtools/server/tests/unit/test_pauselifetime-01.js new file mode 100644 index 000000000..71c2ddae7 --- /dev/null +++ b/devtools/server/tests/unit/test_pauselifetime-01.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that pause-lifetime grips go away correctly after a resume. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let pauseActor = aPacket.actor; + + // Make a bogus request to the pause-liftime actor. Should get + // unrecognized-packet-type (and not no-such-actor). + gClient.request({ to: pauseActor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "unrecognizedPacketType"); + + gThreadClient.resume(function () { + // Now that we've resumed, should get no-such-actor for the + // same request. + gClient.request({ to: pauseActor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + finishClient(gClient); + }); + }); + + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe() { + debugger; + } + stopMe(); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_pauselifetime-02.js b/devtools/server/tests/unit/test_pauselifetime-02.js new file mode 100644 index 000000000..6c90725bb --- /dev/null +++ b/devtools/server/tests/unit/test_pauselifetime-02.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that pause-lifetime grips go away correctly after a resume. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + let objActor = args[0].actor; + do_check_eq(args[0].class, "Object"); + do_check_true(!!objActor); + + // Make a bogus request to the grip actor. Should get + // unrecognized-packet-type (and not no-such-actor). + gClient.request({ to: objActor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "unrecognizedPacketType"); + + gThreadClient.resume(function () { + // Now that we've resumed, should get no-such-actor for the + // same request. + gClient.request({ to: objActor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + finishClient(gClient); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aObject) { + debugger; + } + stopMe({ foo: "bar" }); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_pauselifetime-03.js b/devtools/server/tests/unit/test_pauselifetime-03.js new file mode 100644 index 000000000..9fca887b7 --- /dev/null +++ b/devtools/server/tests/unit/test_pauselifetime-03.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that pause-lifetime grip clients are marked invalid after a resume. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + let objActor = args[0].actor; + do_check_eq(args[0].class, "Object"); + do_check_true(!!objActor); + + let objClient = gThreadClient.pauseGrip(args[0]); + do_check_true(objClient.valid); + + // Make a bogus request to the grip actor. Should get + // unrecognized-packet-type (and not no-such-actor). + gClient.request({ to: objActor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "unrecognizedPacketType"); + do_check_true(objClient.valid); + + gThreadClient.resume(function () { + // Now that we've resumed, should get no-such-actor for the + // same request. + gClient.request({ to: objActor, type: "bogusRequest" }, function (aResponse) { + do_check_false(objClient.valid); + do_check_eq(aResponse.error, "noSuchActor"); + finishClient(gClient); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aObject) { + debugger; + } + stopMe({ foo: "bar" }); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_pauselifetime-04.js b/devtools/server/tests/unit/test_pauselifetime-04.js new file mode 100644 index 000000000..c863da921 --- /dev/null +++ b/devtools/server/tests/unit/test_pauselifetime-04.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that requesting a pause actor for the same value multiple + * times returns the same actor. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-stack"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_pause_frame(); + }); + }); + do_test_pending(); +} + +function test_pause_frame() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let args = aPacket.frame.arguments; + let objActor1 = args[0].actor; + + gThreadClient.getFrames(0, 1, function (aResponse) { + let frame = aResponse.frames[0]; + do_check_eq(objActor1, frame.arguments[0].actor); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(aObject) { + debugger; + } + stopMe({ foo: "bar" }); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_profiler_activation-01.js b/devtools/server/tests/unit/test_profiler_activation-01.js new file mode 100644 index 000000000..31efbb5e3 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_activation-01.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether the profiler module and actor have the correct state on + * initialization, activation, and when a clients' connection closes. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const MAX_PROFILER_ENTRIES = 10000000; + +function run_test() +{ + // Ensure the profiler is not running when the test starts (it could + // happen if the MOZ_PROFILER_STARTUP environment variable is set). + Profiler.StopProfiler(); + + get_chrome_actors((client1, form1) => { + let actor1 = form1.profilerActor; + get_chrome_actors((client2, form2) => { + let actor2 = form2.profilerActor; + test_activate(client1, actor1, client2, actor2, () => { + do_test_finished(); + }); + }); + }); + + do_test_pending(); +} + +function test_activate(client1, actor1, client2, actor2, callback) { + // Profiler should be inactive at this point. + client1.request({ to: actor1, type: "isActive" }, response => { + do_check_false(Profiler.IsActive()); + do_check_false(response.isActive); + do_check_eq(response.currentTime, undefined); + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + + // Start the profiler on the first connection.... + client1.request({ to: actor1, type: "startProfiler", entries: MAX_PROFILER_ENTRIES }, response => { + do_check_true(Profiler.IsActive()); + do_check_true(response.started); + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + do_check_true(response.position >= 0 && response.position < response.totalSize); + do_check_true(response.totalSize === MAX_PROFILER_ENTRIES); + + // On the next connection just make sure the actor has been instantiated. + client2.request({ to: actor2, type: "isActive" }, response => { + do_check_true(Profiler.IsActive()); + do_check_true(response.isActive); + do_check_true(response.currentTime > 0); + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + do_check_true(response.position >= 0 && response.position < response.totalSize); + do_check_true(response.totalSize === MAX_PROFILER_ENTRIES); + + let origConnectionClosed = DebuggerServer._connectionClosed; + + DebuggerServer._connectionClosed = function (conn) { + origConnectionClosed.call(this, conn); + + // The first client is the only actor that started the profiler, + // however the second client can request the accumulated profile data + // at any moment, so the profiler module shouldn't have deactivated. + do_check_true(Profiler.IsActive()); + + DebuggerServer._connectionClosed = function (conn) { + origConnectionClosed.call(this, conn); + + // Now there are no open clients at all, it should *definitely* + // be deactivated by now. + do_check_false(Profiler.IsActive()); + + callback(); + }; + client2.close(); + }; + client1.close(); + }); + }); + }); +} diff --git a/devtools/server/tests/unit/test_profiler_activation-02.js b/devtools/server/tests/unit/test_profiler_activation-02.js new file mode 100644 index 000000000..cf06b1e06 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_activation-02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether the profiler actor correctly handles the case where the + * built-in module was already started. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const WAIT_TIME = 1000; // ms + +function run_test() +{ + // Ensure the profiler is already running when the test starts. + Profiler.StartProfiler(1000000, 1, ["js"], 1); + + DevToolsUtils.waitForTime(WAIT_TIME).then(() => { + + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + test_start_time(client, actor, () => { + client.close().then(do_test_finished); + }); + }); + }); + + do_test_pending(); +} + +function test_start_time(client, actor, callback) { + // Profiler should already be active at this point. + client.request({ to: actor, type: "isActive" }, firstResponse => { + do_check_true(Profiler.IsActive()); + do_check_true(firstResponse.isActive); + do_check_true(firstResponse.currentTime > 0); + + client.request({ to: actor, type: "getProfile" }, secondResponse => { + do_check_true("profile" in secondResponse); + do_check_true(secondResponse.currentTime > firstResponse.currentTime); + + callback(); + }); + }); +} diff --git a/devtools/server/tests/unit/test_profiler_bufferstatus.js b/devtools/server/tests/unit/test_profiler_bufferstatus.js new file mode 100644 index 000000000..9c86bf817 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_bufferstatus.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the profiler actor returns its buffer status via getBufferInfo. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const INITIAL_WAIT_TIME = 100; // ms +const MAX_WAIT_TIME = 20000; // ms +const MAX_PROFILER_ENTRIES = 10000000; + +function run_test() +{ + // Ensure the profiler is not running when the test starts (it could + // happen if the MOZ_PROFILER_STARTUP environment variable is set). + Profiler.StopProfiler(); + + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + check_empty_buffer(client, actor, () => { + activate_profiler(client, actor, startTime => { + wait_for_samples(client, actor, () => { + check_buffer(client, actor, () => { + deactivate_profiler(client, actor, () => { + client.close().then(do_test_finished); + }); + }); + }); + }); + }); + }); + + do_test_pending(); +} + +function check_buffer(client, actor, callback) +{ + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + do_check_true(response.position > 0 && response.position < response.totalSize); + do_check_true(response.totalSize === MAX_PROFILER_ENTRIES); + // There's no way we'll fill the buffer in this test. + do_check_true(response.generation === 0); + + callback(); + }); +} + +function check_empty_buffer(client, actor, callback) +{ + client.request({ to: actor, type: "isActive" }, response => { + do_check_false(Profiler.IsActive()); + do_check_false(response.isActive); + do_check_true(response.position === void 0); + do_check_true(response.totalSize === void 0); + do_check_true(response.generation === void 0); + do_check_false(response.isActive); + do_check_eq(response.currentTime, undefined); + calback(); + }); +} + +function activate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "startProfiler", entries: MAX_PROFILER_ENTRIES }, response => { + do_check_true(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(response.isActive); + callback(response.currentTime); + }); + }); +} + +function deactivate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "stopProfiler" }, response => { + do_check_false(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_false(response.isActive); + callback(); + }); + }); +} + +function wait_for_samples(client, actor, callback) +{ + function attempt(delay) + { + // No idea why, but Components.stack.sourceLine returns null. + let funcLine = Components.stack.lineNumber - 3; + + // Spin for the requested time, then take a sample. + let start = Date.now(); + let stack; + do_print("Attempt: delay = " + delay); + while (Date.now() - start < delay) { stack = Components.stack; } + do_print("Attempt: finished waiting."); + + client.request({ to: actor, type: "getProfile" }, response => { + // At this point, we may or may not have samples, depending on + // whether the spin loop above has given the profiler enough time + // to get started. + if (response.profile.threads[0].samples.length == 0) { + if (delay < MAX_WAIT_TIME) { + // Double the spin-wait time and try again. + do_print("Attempt: no samples, going around again."); + return attempt(delay * 2); + } else { + // We've waited long enough, so just fail. + do_print("Attempt: waited a long time, but no samples were collected."); + do_print("Giving up."); + do_check_true(false); + return; + } + } + callback(); + }); + } + + // Start off with a 100 millisecond delay. + attempt(INITIAL_WAIT_TIME); +} diff --git a/devtools/server/tests/unit/test_profiler_close.js b/devtools/server/tests/unit/test_profiler_close.js new file mode 100644 index 000000000..a8b3040fd --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_close.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether the profiler module is kept active when there are multiple + * client consumers and one requests deactivation. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); + +function run_test() +{ + get_chrome_actors((client1, form1) => { + let actor1 = form1.profilerActor; + get_chrome_actors((client2, form2) => { + let actor2 = form2.profilerActor; + test_close(client1, actor1, client2, actor2, () => { + client1.close(() => { + client2.close(() => { + do_test_finished(); + }); + }); + }); + }); + }); + + do_test_pending(); +} + +function activate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "startProfiler" }, response => { + do_check_true(response.started); + do_check_true(Profiler.IsActive()); + + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(response.isActive); + callback(); + }); + }); +} + +function deactivate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "stopProfiler" }, response => { + do_check_false(response.started); + do_check_true(Profiler.IsActive()); + + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(response.isActive); + callback(); + }); + }); +} + +function test_close(client1, actor1, client2, actor2, callback) +{ + activate_profiler(client1, actor1, () => { + activate_profiler(client2, actor2, () => { + deactivate_profiler(client1, actor1, () => { + deactivate_profiler(client2, actor2, () => { + callback(); + }); + }); + }); + }); +} diff --git a/devtools/server/tests/unit/test_profiler_data.js b/devtools/server/tests/unit/test_profiler_data.js new file mode 100644 index 000000000..2a79eed1f --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_data.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the profiler actor can correctly retrieve a profile after + * it is activated. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const INITIAL_WAIT_TIME = 100; // ms +const MAX_WAIT_TIME = 20000; // ms + +function run_test() +{ + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + activate_profiler(client, actor, startTime => { + test_data(client, actor, startTime, () => { + deactivate_profiler(client, actor, () => { + client.close().then(do_test_finished); + }); + }); + }); + }); + + do_test_pending(); +} + +function activate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "startProfiler" }, response => { + do_check_true(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(response.isActive); + callback(response.currentTime); + }); + }); +} + +function deactivate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "stopProfiler" }, response => { + do_check_false(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_false(response.isActive); + callback(); + }); + }); +} + +function test_data(client, actor, startTime, callback) +{ + function attempt(delay) + { + // No idea why, but Components.stack.sourceLine returns null. + let funcLine = Components.stack.lineNumber - 3; + + // Spin for the requested time, then take a sample. + let start = Date.now(); + let stack; + do_print("Attempt: delay = " + delay); + while (Date.now() - start < delay) { stack = Components.stack; } + do_print("Attempt: finished waiting."); + + client.request({ to: actor, type: "getProfile", startTime }, response => { + // Any valid getProfile response should have the following top + // level structure. + do_check_eq(typeof response.profile, "object"); + do_check_eq(typeof response.profile.meta, "object"); + do_check_eq(typeof response.profile.meta.platform, "string"); + do_check_eq(typeof response.profile.threads, "object"); + do_check_eq(typeof response.profile.threads[0], "object"); + do_check_eq(typeof response.profile.threads[0].samples, "object"); + + // At this point, we may or may not have samples, depending on + // whether the spin loop above has given the profiler enough time + // to get started. + if (response.profile.threads[0].samples.length == 0) { + if (delay < MAX_WAIT_TIME) { + // Double the spin-wait time and try again. + do_print("Attempt: no samples, going around again."); + return attempt(delay * 2); + } else { + // We've waited long enough, so just fail. + do_print("Attempt: waited a long time, but no samples were collected."); + do_print("Giving up."); + do_check_true(false); + return; + } + } + + // Now check the samples. At least one sample is expected to + // have been in the busy wait above. + let loc = stack.name + " (" + stack.filename + ":" + funcLine + ")"; + let thread0 = response.profile.threads[0]; + do_check_true(thread0.samples.data.some(sample => { + let frames = getInflatedStackLocations(thread0, sample); + return frames.length != 0 && + frames.some(location => (location == loc)); + })); + + callback(); + }); + } + + // Start off with a 100 millisecond delay. + attempt(INITIAL_WAIT_TIME); +} diff --git a/devtools/server/tests/unit/test_profiler_events-01.js b/devtools/server/tests/unit/test_profiler_events-01.js new file mode 100644 index 000000000..b8ca592b9 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_events-01.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the event notification service for the profiler actor. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const { ProfilerFront } = require("devtools/shared/fronts/profiler"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let [client, form] = yield getChromeActors(); + let front = new ProfilerFront(client, form); + + let events = [0, 0, 0, 0]; + front.on("console-api-profiler", () => events[0]++); + front.on("profiler-started", () => events[1]++); + front.on("profiler-stopped", () => events[2]++); + client.addListener("eventNotification", (type, response) => { + do_check_true(type === "eventNotification"); + events[3]++; + }); + + yield front.startProfiler(); + yield front.stopProfiler(); + + // All should be empty without binding events + do_check_true(events[0] === 0); + do_check_true(events[1] === 0); + do_check_true(events[2] === 0); + do_check_true(events[3] === 0); + + let ret = yield front.registerEventNotifications({ events: ["console-api-profiler", "profiler-started", "profiler-stopped"] }); + do_check_true(ret.registered.length === 3); + + yield front.startProfiler(); + do_check_true(events[0] === 0); + do_check_true(events[1] === 1); + do_check_true(events[2] === 0); + do_check_true(events[3] === 1, "compatibility events supported for eventNotifications"); + + yield front.stopProfiler(); + do_check_true(events[0] === 0); + do_check_true(events[1] === 1); + do_check_true(events[2] === 1); + do_check_true(events[3] === 2, "compatibility events supported for eventNotifications"); + + ret = yield front.unregisterEventNotifications({ events: ["console-api-profiler", "profiler-started", "profiler-stopped"] }); + do_check_true(ret.registered.length === 3); +}); + +function getChromeActors() { + let deferred = promise.defer(); + get_chrome_actors((client, form) => deferred.resolve([client, form])); + return deferred.promise; +} diff --git a/devtools/server/tests/unit/test_profiler_events-02.js b/devtools/server/tests/unit/test_profiler_events-02.js new file mode 100644 index 000000000..fed702043 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_events-02.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the event notification service for the profiler actor. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const MAX_PROFILER_ENTRIES = 10000000; +const { ProfilerFront } = require("devtools/shared/fronts/profiler"); +const { waitForTime } = DevToolsUtils; + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let [client, form] = yield getChromeActors(); + let front = new ProfilerFront(client, form); + + // Ensure the profiler is not running when the test starts (it could + // happen if the MOZ_PROFILER_STARTUP environment variable is set). + Profiler.StopProfiler(); + let eventsCalled = 0; + let handledThreeTimes = promise.defer(); + + front.on("profiler-status", (response) => { + dump("'profiler-status' fired\n"); + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + do_check_true(response.position > 0 && response.position < response.totalSize); + do_check_true(response.totalSize === MAX_PROFILER_ENTRIES); + // There's no way we'll fill the buffer in this test. + do_check_true(response.generation === 0); + + eventsCalled++; + if (eventsCalled > 2) { + handledThreeTimes.resolve(); + } + }); + + yield front.setProfilerStatusInterval(1); + dump("Set the profiler-status event interval to 1\n"); + yield front.startProfiler(); + yield waitForTime(500); + yield front.stopProfiler(); + + do_check_true(eventsCalled === 0, "No 'profiler-status' events should be fired before registering."); + + let ret = yield front.registerEventNotifications({ events: ["profiler-status"] }); + do_check_true(ret.registered.length === 1); + + yield front.startProfiler(); + yield handledThreeTimes.promise; + yield front.stopProfiler(); + do_check_true(eventsCalled >= 3, "profiler-status fired atleast three times while recording"); + + let totalEvents = eventsCalled; + yield waitForTime(50); + do_check_true(totalEvents === eventsCalled, "No more profiler-status events after recording."); +}); + +function getChromeActors() { + let deferred = promise.defer(); + get_chrome_actors((client, form) => deferred.resolve([client, form])); + return deferred.promise; +} diff --git a/devtools/server/tests/unit/test_profiler_getbufferinfo.js b/devtools/server/tests/unit/test_profiler_getbufferinfo.js new file mode 100644 index 000000000..1ec536738 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_getbufferinfo.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the profiler actor returns its buffer status via getBufferInfo. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +const INITIAL_WAIT_TIME = 100; // ms +const MAX_WAIT_TIME = 20000; // ms +const MAX_PROFILER_ENTRIES = 10000000; + +function run_test() +{ + // Ensure the profiler is not running when the test starts (it could + // happen if the MOZ_PROFILER_STARTUP environment variable is set). + Profiler.StopProfiler(); + + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + check_empty_buffer(client, actor, () => { + activate_profiler(client, actor, startTime => { + wait_for_samples(client, actor, () => { + check_buffer(client, actor, () => { + deactivate_profiler(client, actor, () => { + client.close().then(do_test_finished); + }); + }); + }); + }); + }); + }); + + do_test_pending(); +} + +function check_empty_buffer(client, actor, callback) +{ + client.request({ to: actor, type: "getBufferInfo" }, response => { + do_check_true(response.position === 0); + do_check_true(response.totalSize === 0); + do_check_true(response.generation === 0); + callback(); + }); +} + +function check_buffer(client, actor, callback) +{ + client.request({ to: actor, type: "getBufferInfo" }, response => { + do_check_true(typeof response.position === "number"); + do_check_true(typeof response.totalSize === "number"); + do_check_true(typeof response.generation === "number"); + do_check_true(response.position > 0 && response.position < response.totalSize); + do_check_true(response.totalSize === MAX_PROFILER_ENTRIES); + // There's no way we'll fill the buffer in this test. + do_check_true(response.generation === 0); + + callback(); + }); +} + +function activate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "startProfiler", entries: MAX_PROFILER_ENTRIES }, response => { + do_check_true(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_true(response.isActive); + callback(response.currentTime); + }); + }); +} + +function deactivate_profiler(client, actor, callback) +{ + client.request({ to: actor, type: "stopProfiler" }, response => { + do_check_false(response.started); + client.request({ to: actor, type: "isActive" }, response => { + do_check_false(response.isActive); + callback(); + }); + }); +} + +function wait_for_samples(client, actor, callback) +{ + function attempt(delay) + { + // No idea why, but Components.stack.sourceLine returns null. + let funcLine = Components.stack.lineNumber - 3; + + // Spin for the requested time, then take a sample. + let start = Date.now(); + let stack; + do_print("Attempt: delay = " + delay); + while (Date.now() - start < delay) { stack = Components.stack; } + do_print("Attempt: finished waiting."); + + client.request({ to: actor, type: "getProfile" }, response => { + // At this point, we may or may not have samples, depending on + // whether the spin loop above has given the profiler enough time + // to get started. + if (response.profile.threads[0].samples.length == 0) { + if (delay < MAX_WAIT_TIME) { + // Double the spin-wait time and try again. + do_print("Attempt: no samples, going around again."); + return attempt(delay * 2); + } else { + // We've waited long enough, so just fail. + do_print("Attempt: waited a long time, but no samples were collected."); + do_print("Giving up."); + do_check_true(false); + return; + } + } + callback(); + }); + } + + // Start off with a 100 millisecond delay. + attempt(INITIAL_WAIT_TIME); +} diff --git a/devtools/server/tests/unit/test_profiler_getfeatures.js b/devtools/server/tests/unit/test_profiler_getfeatures.js new file mode 100644 index 000000000..5b37e7d55 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_getfeatures.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether the profiler responds to "getFeatures" adequately. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); + +function run_test() +{ + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + test_getfeatures(client, actor, () => { + client.close().then(() => { + do_test_finished(); + }); + }); + }); + + do_test_pending(); +} + +function test_getfeatures(client, actor, callback) +{ + client.request({ to: actor, type: "getFeatures" }, response => { + do_check_eq(typeof response.features, "object"); + do_check_true(response.features.length >= 1); + do_check_eq(typeof response.features[0], "string"); + do_check_true(response.features.indexOf("js") != -1); + callback(); + }); +} diff --git a/devtools/server/tests/unit/test_profiler_getsharedlibraryinformation.js b/devtools/server/tests/unit/test_profiler_getsharedlibraryinformation.js new file mode 100644 index 000000000..a36577320 --- /dev/null +++ b/devtools/server/tests/unit/test_profiler_getsharedlibraryinformation.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether the profiler responds to "getSharedLibraryInformation" adequately. + */ + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); + +function run_test() +{ + get_chrome_actors((client, form) => { + let actor = form.profilerActor; + test_getsharedlibraryinformation(client, actor, () => { + client.close().then(() => { + do_test_finished(); + }); + }); + }); + + do_test_pending(); +} + +function test_getsharedlibraryinformation(client, actor, callback) +{ + client.request({ to: actor, type: "getSharedLibraryInformation" }, response => { + do_check_eq(typeof response.sharedLibraryInformation, "string"); + let libs = []; + try { + libs = JSON.parse(response.sharedLibraryInformation); + } catch (e) { + do_check_true(false); + } + do_check_eq(typeof libs, "object"); + do_check_true(libs.length >= 1); + do_check_eq(typeof libs[0], "object"); + do_check_eq(typeof libs[0].name, "string"); + do_check_eq(typeof libs[0].start, "number"); + do_check_eq(typeof libs[0].end, "number"); + do_check_true(libs[0].start <= libs[0].end); + callback(); + }); +} diff --git a/devtools/server/tests/unit/test_promise_state-01.js b/devtools/server/tests/unit/test_promise_state-01.js new file mode 100644 index 000000000..a525560ab --- /dev/null +++ b/devtools/server/tests/unit/test_promise_state-01.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * pending. + */ + +function run_test() +{ + initTestDebuggerServer(); + const debuggee = addTestGlobal("test-promise-state"); + const client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect().then(function () { + attachTestTabAndResume(client, "test-promise-state", function (response, tabClient, threadClient) { + Task.spawn(function* () { + const packet = yield executeOnNextTickAndWaitForPause(() => evalCode(debuggee), client); + + const grip = packet.frame.environment.bindings.variables.p; + ok(grip.value.preview); + equal(grip.value.class, "Promise"); + equal(grip.value.promiseState.state, "pending"); + + finishClient(client); + }); + }); + }); + do_test_pending(); +} + +function evalCode(debuggee) { + Components.utils.evalInSandbox( + "doTest();\n" + + function doTest() { + var p = new Promise(function () {}); + debugger; + }, + debuggee + ); +} diff --git a/devtools/server/tests/unit/test_promise_state-02.js b/devtools/server/tests/unit/test_promise_state-02.js new file mode 100644 index 000000000..cf44f1946 --- /dev/null +++ b/devtools/server/tests/unit/test_promise_state-02.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * fulfilled. + */ + +function run_test() +{ + initTestDebuggerServer(); + const debuggee = addTestGlobal("test-promise-state"); + const client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect().then(function () { + attachTestTabAndResume(client, "test-promise-state", function (response, tabClient, threadClient) { + Task.spawn(function* () { + const packet = yield executeOnNextTickAndWaitForPause(() => evalCode(debuggee), client); + + const grip = packet.frame.environment.bindings.variables.p; + ok(grip.value.preview); + equal(grip.value.class, "Promise"); + equal(grip.value.promiseState.state, "fulfilled"); + equal(grip.value.promiseState.value.actorID, packet.frame.arguments[0].actorID, + "The promise's fulfilled state value should be the same value passed to the then function"); + + finishClient(client); + }); + }); + }); + do_test_pending(); +} + +function evalCode(debuggee) { + Components.utils.evalInSandbox( + "doTest();\n" + + function doTest() { + var resolved = Promise.resolve({}); + resolved.then(() => { + var p = resolved; + debugger; + }); + }, + debuggee + ); +} diff --git a/devtools/server/tests/unit/test_promise_state-03.js b/devtools/server/tests/unit/test_promise_state-03.js new file mode 100644 index 000000000..cf64e3e27 --- /dev/null +++ b/devtools/server/tests/unit/test_promise_state-03.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * rejected. + */ + +function run_test() +{ + initTestDebuggerServer(); + const debuggee = addTestGlobal("test-promise-state"); + const client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect().then(function () { + attachTestTabAndResume(client, "test-promise-state", function (response, tabClient, threadClient) { + Task.spawn(function* () { + const packet = yield executeOnNextTickAndWaitForPause(() => evalCode(debuggee), client); + + const grip = packet.frame.environment.bindings.variables.p; + ok(grip.value.preview); + equal(grip.value.class, "Promise"); + equal(grip.value.promiseState.state, "rejected"); + equal(grip.value.promiseState.reason.actorID, packet.frame.arguments[0].actorID, + "The promise's rejected state reason should be the same value passed to the then function"); + + finishClient(client); + }); + }); + }); + do_test_pending(); +} + +function evalCode(debuggee) { + Components.utils.evalInSandbox( + "doTest();\n" + + function doTest() { + var resolved = Promise.reject(new Error("uh oh")); + resolved.then(null, () => { + var p = resolved; + debugger; + }); + }, + debuggee + ); +} diff --git a/devtools/server/tests/unit/test_promises_actor_attach.js b/devtools/server/tests/unit/test_promises_actor_attach.js new file mode 100644 index 000000000..17c2a1f41 --- /dev/null +++ b/devtools/server/tests/unit/test_promises_actor_attach.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can attach and detach to the PromisesActor under the correct + * states. + */ + +const { PromisesFront } = require("devtools/shared/fronts/promises"); + +add_task(function* () { + let client = yield startTestDebuggerServer("promises-actor-test"); + let chromeActors = yield getChromeActors(client); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testAttach(client, chromeActors); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-actor-test"); + ok(targetTab, "Found our target tab."); + + let [ tabResponse ] = yield attachTab(client, targetTab); + + yield testAttach(client, tabResponse); + + yield close(client); +}); + +function* testAttach(client, parent) { + let promises = PromisesFront(client, parent); + + try { + yield promises.detach(); + ok(false, "Should not be able to detach when in a detached state."); + } catch (e) { + ok(true, "Expected detach to fail when already in a detached state."); + } + + yield promises.attach(); + ok(true, "Expected attach to succeed."); + + try { + yield promises.attach(); + ok(false, "Should not be able to attach when in an attached state."); + } catch (e) { + ok(true, "Expected attach to fail when already in an attached state."); + } + + yield promises.detach(); + ok(true, "Expected detach to succeed."); +} diff --git a/devtools/server/tests/unit/test_promises_actor_exist.js b/devtools/server/tests/unit/test_promises_actor_exist.js new file mode 100644 index 000000000..13eef3e99 --- /dev/null +++ b/devtools/server/tests/unit/test_promises_actor_exist.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the PromisesActor exists in the TabActors and ChromeActors. + */ + +add_task(function* () { + let client = yield startTestDebuggerServer("promises-actor-test"); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-actor-test"); + ok(targetTab, "Found our target tab."); + + // Attach to the TabActor and check the response + client.request({ to: targetTab.actor, type: "attach" }, response => { + ok(!("error" in response), "Expect no error in response."); + ok(response.from, targetTab.actor, + "Expect the target TabActor in response form field."); + ok(response.type, "tabAttached", + "Expect tabAttached in the response type."); + is(typeof response.promisesActor === "string", + "Should have a tab context PromisesActor."); + }); + + let chromeActors = yield getChromeActors(client); + ok(typeof chromeActors.promisesActor === "string", + "Should have a chrome context PromisesActor."); +}); diff --git a/devtools/server/tests/unit/test_promises_actor_list_promises.js b/devtools/server/tests/unit/test_promises_actor_list_promises.js new file mode 100644 index 000000000..f5b273121 --- /dev/null +++ b/devtools/server/tests/unit/test_promises_actor_list_promises.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get the list of all live Promise objects from the + * PromisesActor. + */ + +"use strict"; + +const { PromisesFront } = require("devtools/shared/fronts/promises"); +const SECRET = "MyLittleSecret"; + +add_task(function* () { + let client = yield startTestDebuggerServer("promises-actor-test"); + let chromeActors = yield getChromeActors(client); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testListPromises(client, chromeActors, v => + new Promise(resolve => resolve(v))); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-actor-test"); + ok(targetTab, "Found our target tab."); + + yield testListPromises(client, targetTab, v => { + const debuggee = DebuggerServer.getTestGlobal("promises-actor-test"); + return debuggee.Promise.resolve(v); + }); + + yield close(client); +}); + +function* testListPromises(client, form, makePromise) { + let resolution = SECRET + Math.random(); + let promise = makePromise(resolution); + let front = PromisesFront(client, form); + + yield front.attach(); + + let promises = yield front.listPromises(); + + let found = false; + for (let p of promises) { + equal(p.type, "object", "Expect type to be Object"); + equal(p.class, "Promise", "Expect class to be Promise"); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number"); + equal(typeof p.promiseState.timeToSettle, "number", + "Expect time to settle to be a number"); + + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + found = true; + } + } + + ok(found, "Found our promise"); + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/devtools/server/tests/unit/test_promises_actor_onnewpromise.js b/devtools/server/tests/unit/test_promises_actor_onnewpromise.js new file mode 100644 index 000000000..04b3e6510 --- /dev/null +++ b/devtools/server/tests/unit/test_promises_actor_onnewpromise.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get the list of all new Promise objects from the + * PromisesActor onNewPromise event handler. + */ + +"use strict"; + +const { PromisesFront } = require("devtools/shared/fronts/promises"); + +var events = require("sdk/event/core"); + +add_task(function* () { + let client = yield startTestDebuggerServer("promises-actor-test"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().includes("native code"), "Expect native DOM Promise"); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testNewPromisesEvent(client, chromeActors, + v => new Promise(resolve => resolve(v))); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-actor-test"); + ok(targetTab, "Found our target tab."); + + yield testNewPromisesEvent(client, targetTab, v => { + const debuggee = DebuggerServer.getTestGlobal("promises-actor-test"); + return debuggee.Promise.resolve(v); + }); + + yield close(client); +}); + +function* testNewPromisesEvent(client, form, makePromise) { + let front = PromisesFront(client, form); + let resolution = "MyLittleSecret" + Math.random(); + let found = false; + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + equal(p.type, "object", "Expect type to be Object"); + equal(p.class, "Promise", "Expect class to be Promise"); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number"); + + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + found = true; + resolve(); + } else { + dump("Found non-target promise\n"); + } + } + }); + }); + + let promise = makePromise(resolution); + + yield onNewPromise; + ok(found, "Found our new promise"); + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js b/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js new file mode 100644 index 000000000..ab4774733 --- /dev/null +++ b/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get the list of Promise objects that have settled from the + * PromisesActor onPromiseSettled event handler. + */ + +"use strict"; + +Cu.import("resource://testing-common/PromiseTestUtils.jsm", this); + +const { PromisesFront } = require("devtools/shared/fronts/promises"); + +var events = require("sdk/event/core"); + +add_task(function* () { + let client = yield startTestDebuggerServer("promises-actor-test"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().includes("native code"), "Expect native DOM Promise"); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testPromisesSettled(client, chromeActors, + v => new Promise(resolve => resolve(v)), + v => new Promise((resolve, reject) => reject(v))); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-actor-test"); + ok(targetTab, "Found our target tab."); + + yield testPromisesSettled(client, targetTab, v => { + const debuggee = DebuggerServer.getTestGlobal("promises-actor-test"); + return debuggee.Promise.resolve(v); + }, v => { + const debuggee = DebuggerServer.getTestGlobal("promises-actor-test"); + return debuggee.Promise.reject(v); + }); + + yield close(client); +}); + +function* testPromisesSettled(client, form, makeResolvePromise, + makeRejectPromise) { + let front = PromisesFront(client, form); + let resolution = "MyLittleSecret" + Math.random(); + + yield front.attach(); + yield front.listPromises(); + + let onPromiseSettled = oncePromiseSettled(front, resolution, true, false); + let resolvedPromise = makeResolvePromise(resolution); + let foundResolvedPromise = yield onPromiseSettled; + ok(foundResolvedPromise, "Found our resolved promise"); + + PromiseTestUtils.expectUncaughtRejection(r => r.message == resolution); + onPromiseSettled = oncePromiseSettled(front, resolution, false, true); + let rejectedPromise = makeRejectPromise(resolution); + let foundRejectedPromise = yield onPromiseSettled; + ok(foundRejectedPromise, "Found our rejected promise"); + + yield front.detach(); + // Appease eslint + void resolvedPromise; + void rejectedPromise; +} + +function oncePromiseSettled(front, resolution, resolveValue, rejectValue) { + return new Promise(resolve => { + events.on(front, "promises-settled", promises => { + for (let p of promises) { + equal(p.type, "object", "Expect type to be Object"); + equal(p.class, "Promise", "Expect class to be Promise"); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number"); + equal(typeof p.promiseState.timeToSettle, "number", + "Expect time to settle to be a number"); + + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + resolve(resolveValue); + } else if (p.promiseState.state === "rejected" && + p.promiseState.reason === resolution) { + resolve(rejectValue); + } else { + dump("Found non-target promise\n"); + } + } + }); + }); +} diff --git a/devtools/server/tests/unit/test_promises_client_getdependentpromises.js b/devtools/server/tests/unit/test_promises_client_getdependentpromises.js new file mode 100644 index 000000000..8900cf81c --- /dev/null +++ b/devtools/server/tests/unit/test_promises_client_getdependentpromises.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get the list of dependent promises from the ObjectClient. + */ + +"use strict"; + +const { PromisesFront } = require("devtools/shared/fronts/promises"); + +var events = require("sdk/event/core"); + +add_task(function* () { + let client = yield startTestDebuggerServer("test-promises-dependentpromises"); + let chromeActors = yield getChromeActors(client); + yield attachTab(client, chromeActors); + + ok(Promise.toString().includes("native code"), "Expect native DOM Promise."); + + yield testGetDependentPromises(client, chromeActors, () => { + let p = new Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + let r = p.then(null, () => {}); + r.name = "r"; + + return p; + }); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "test-promises-dependentpromises"); + ok(targetTab, "Found our target tab."); + yield attachTab(client, targetTab); + + yield testGetDependentPromises(client, targetTab, () => { + const debuggee = + DebuggerServer.getTestGlobal("test-promises-dependentpromises"); + + let p = new debuggee.Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + let r = p.then(null, () => {}); + r.name = "r"; + + return p; + }); + + yield close(client); +}); + +function* testGetDependentPromises(client, form, makePromises) { + let front = PromisesFront(client, form); + + yield front.attach(); + yield front.listPromises(); + + // Get the grip for promise p + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.preview.ownProperties.name && + p.preview.ownProperties.name.value === "p") { + resolve(p); + } + } + }); + }); + + let promise = makePromises(); + + let grip = yield onNewPromise; + ok(grip, "Found our promise p."); + + let objectClient = new ObjectClient(client, grip); + ok(objectClient, "Got Object Client."); + + // Get the dependent promises for promise p and assert that the list of + // dependent promises is correct + yield new Promise(resolve => { + objectClient.getDependentPromises(response => { + let dependentNames = response.promises.map(p => + p.preview.ownProperties.name.value); + let expectedDependentNames = ["q", "r"]; + + equal(dependentNames.length, expectedDependentNames.length, + "Got expected number of dependent promises."); + + for (let i = 0; i < dependentNames.length; i++) { + equal(dependentNames[i], expectedDependentNames[i], + "Got expected dependent name."); + } + + for (let p of response.promises) { + equal(p.type, "object", "Expect type to be Object."); + equal(p.class, "Promise", "Expect class to be Promise."); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number."); + ok(!p.promiseState.timeToSettle, + "Expect time to settle to be undefined."); + } + + resolve(); + }); + }); + + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/devtools/server/tests/unit/test_promises_object_creationtimestamp.js b/devtools/server/tests/unit/test_promises_object_creationtimestamp.js new file mode 100644 index 000000000..1360be56a --- /dev/null +++ b/devtools/server/tests/unit/test_promises_object_creationtimestamp.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get the approximate time range for promise creation timestamp. + */ + +"use strict"; + +const { PromisesFront } = require("devtools/shared/fronts/promises"); + +var events = require("sdk/event/core"); + +add_task(function* () { + let client = yield startTestDebuggerServer("promises-object-test"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().includes("native code"), "Expect native DOM Promise."); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testPromiseCreationTimestamp(client, chromeActors, v => { + return new Promise(resolve => resolve(v)); + }); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-object-test"); + ok(targetTab, "Found our target tab."); + + yield testPromiseCreationTimestamp(client, targetTab, v => { + const debuggee = DebuggerServer.getTestGlobal("promises-object-test"); + return debuggee.Promise.resolve(v); + }); + + yield close(client); +}); + +function* testPromiseCreationTimestamp(client, form, makePromise) { + let front = PromisesFront(client, form); + let resolution = "MyLittleSecret" + Math.random(); + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + resolve(p); + } + } + }); + }); + + let start = Date.now(); + let promise = makePromise(resolution); + let end = Date.now(); + + let grip = yield onNewPromise; + ok(grip, "Found our new promise."); + + let creationTimestamp = grip.promiseState.creationTimestamp; + + ok(start - 1 <= creationTimestamp && creationTimestamp <= end + 1, + "Expect promise creation timestamp to be within elapsed time range."); + + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/devtools/server/tests/unit/test_promises_object_timetosettle-01.js b/devtools/server/tests/unit/test_promises_object_timetosettle-01.js new file mode 100644 index 000000000..1b3240e3d --- /dev/null +++ b/devtools/server/tests/unit/test_promises_object_timetosettle-01.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test whether or not we get the time to settle depending on the state of the + * promise. + */ + +"use strict"; + +const { PromisesFront } = require("devtools/shared/fronts/promises"); + +var events = require("sdk/event/core"); + +add_task(function* () { + let client = yield startTestDebuggerServer("test-promises-timetosettle"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().includes("native code"), "Expect native DOM Promise."); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testGetTimeToSettle(client, chromeActors, () => { + let p = new Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + + return p; + }); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "test-promises-timetosettle"); + ok(targetTab, "Found our target tab."); + + yield testGetTimeToSettle(client, targetTab, () => { + const debuggee = + DebuggerServer.getTestGlobal("test-promises-timetosettle"); + + let p = new debuggee.Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + + return p; + }); + + yield close(client); +}); + +function* testGetTimeToSettle(client, form, makePromises) { + let front = PromisesFront(client, form); + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.promiseState.state === "pending") { + ok(!p.promiseState.timeToSettle, + "Expect no time to settle for unsettled promise."); + } else { + ok(p.promiseState.timeToSettle, + "Expect time to settle for settled promise."); + equal(typeof p.promiseState.timeToSettle, "number", + "Expect time to settle to be a number."); + } + } + resolve(); + }); + }); + + let promise = makePromises(); + + yield onNewPromise; + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/devtools/server/tests/unit/test_promises_object_timetosettle-02.js b/devtools/server/tests/unit/test_promises_object_timetosettle-02.js new file mode 100644 index 000000000..10224d0b9 --- /dev/null +++ b/devtools/server/tests/unit/test_promises_object_timetosettle-02.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get the expected settlement time for promise time to settle. + */ + +"use strict"; + +const { PromisesFront } = require("devtools/shared/fronts/promises"); +const { setTimeout } = require("sdk/timers"); + +var events = require("sdk/event/core"); + +add_task(function* () { + let client = yield startTestDebuggerServer("test-promises-timetosettle"); + let chromeActors = yield getChromeActors(client); + yield attachTab(client, chromeActors); + + ok(Promise.toString().includes("native code"), "Expect native DOM Promise."); + + // We have to attach the chrome TabActor before playing with the PromiseActor + yield attachTab(client, chromeActors); + yield testGetTimeToSettle(client, chromeActors, + v => new Promise(resolve => setTimeout(() => resolve(v), 100))); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "test-promises-timetosettle"); + ok(targetTab, "Found our target tab."); + yield attachTab(client, targetTab); + + yield testGetTimeToSettle(client, targetTab, v => { + const debuggee = + DebuggerServer.getTestGlobal("test-promises-timetosettle"); + return new debuggee.Promise(resolve => setTimeout(() => resolve(v), 100)); + }); + + yield close(client); +}); + +function* testGetTimeToSettle(client, form, makePromise) { + let front = PromisesFront(client, form); + let resolution = "MyLittleSecret" + Math.random(); + let found = false; + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "promises-settled", promises => { + for (let p of promises) { + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + let timeToSettle = Math.floor(p.promiseState.timeToSettle / 100) * 100; + ok(timeToSettle >= 100, + "Expect time to settle for resolved promise to be " + + "at least 100ms, got " + timeToSettle + "ms."); + found = true; + resolve(); + } else { + dump("Found non-target promise.\n"); + } + } + }); + }); + + let promise = makePromise(resolution); + + yield onNewPromise; + ok(found, "Found our new promise."); + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/devtools/server/tests/unit/test_protocolSpec.js b/devtools/server/tests/unit/test_protocolSpec.js new file mode 100644 index 000000000..cc0746387 --- /dev/null +++ b/devtools/server/tests/unit/test_protocolSpec.js @@ -0,0 +1,17 @@ +const run_test = Test(function* () { + initTestDebuggerServer(); + const connection = DebuggerServer.connectPipe(); + const client = Async(new DebuggerClient(connection)); + + yield client.connect(); + + const response = yield client.request({ + to: "root", + type: "protocolDescription" + }); + + assert(response.from == "root"); + assert(typeof (response.types) === "object"); + + yield client.close(); +}); diff --git a/devtools/server/tests/unit/test_protocol_abort.js b/devtools/server/tests/unit/test_protocol_abort.js new file mode 100644 index 000000000..bb25d1b2c --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_abort.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Outstanding requests should be rejected when the connection aborts + * unexpectedly. + */ + +var protocol = require("devtools/shared/protocol"); +var {Arg, Option, RetVal} = protocol; +var events = require("sdk/event/core"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + }; +} + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + methods: { + simpleReturn: { + response: { value: RetVal() } + } + } +}); + +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + typeName: "root", + initialize: function (conn) { + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + this.actorID = "root"; + this.sequence = 0; + }, + + sayHello: simpleHello, + + simpleReturn: function () { + return this.sequence++; + } +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + initialize: function (client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root owns itself. + this.manage(this); + } +}); + +function run_test() { + DebuggerServer.createRootActor = RootActor; + DebuggerServer.init(); + + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + let rootClient; + + client.connect().then(([applicationType, traits]) => { + rootClient = RootFront(client); + + rootClient.simpleReturn().then(() => { + ok(false, "Connection was aborted, request shouldn't resolve"); + do_test_finished(); + }, e => { + let error = e.toString(); + ok(true, "Connection was aborted, request rejected correctly"); + ok(error.includes("Request stack:"), "Error includes request stack"); + ok(error.includes("test_protocol_abort.js"), "Stack includes this test"); + do_test_finished(); + }); + + trace.close(); + }); + + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_protocol_async.js b/devtools/server/tests/unit/test_protocol_async.js new file mode 100644 index 000000000..75f053863 --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_async.js @@ -0,0 +1,184 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure we get replies in the same order that we sent their + * requests even when earlier requests take several event ticks to + * complete. + */ + +var protocol = require("devtools/shared/protocol"); +var {Arg, Option, RetVal} = protocol; +var events = require("sdk/event/core"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + }; +} + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + methods: { + simpleReturn: { + response: { value: RetVal() }, + }, + promiseReturn: { + request: { toWait: Arg(0, "number") }, + response: { value: RetVal("number") }, + }, + simpleThrow: { + response: { value: RetVal("number") } + }, + promiseThrow: { + response: { value: RetVal("number") }, + } + } +}); + +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + initialize: function (conn) { + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + this.actorID = "root"; + this.sequence = 0; + }, + + sayHello: simpleHello, + + simpleReturn: function () { + return this.sequence++; + }, + + promiseReturn: function (toWait) { + // Guarantee that this resolves after simpleReturn returns. + let deferred = promise.defer(); + let sequence = this.sequence++; + + // Wait until the number of requests specified by toWait have + // happened, to test queuing. + let check = () => { + if ((this.sequence - sequence) < toWait) { + do_execute_soon(check); + return; + } + deferred.resolve(sequence); + }; + do_execute_soon(check); + + return deferred.promise; + }, + + simpleThrow: function () { + throw new Error(this.sequence++); + }, + + promiseThrow: function () { + // Guarantee that this resolves after simpleReturn returns. + let deferred = promise.defer(); + let sequence = this.sequence++; + // This should be enough to force a failure if the code is broken. + do_timeout(150, () => { + deferred.reject(sequence++); + }); + return deferred.promise; + } +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + initialize: function (client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root owns itself. + this.manage(this); + } +}); + +function run_test() +{ + DebuggerServer.createRootActor = RootActor; + DebuggerServer.init(); + + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + let rootClient; + + client.connect().then(([applicationType, traits]) => { + rootClient = RootFront(client); + + let calls = []; + let sequence = 0; + + // Execute a call that won't finish processing until 2 + // more calls have happened + calls.push(rootClient.promiseReturn(2).then(ret => { + do_check_eq(sequence, 0); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + // Put a few requests into the backlog + + calls.push(rootClient.simpleReturn().then(ret => { + do_check_eq(sequence, 1); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + calls.push(rootClient.simpleReturn().then(ret => { + do_check_eq(sequence, 2); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + calls.push(rootClient.simpleThrow().then(() => { + do_check_true(false, "simpleThrow shouldn't succeed!"); + }, error => { + do_check_eq(sequence++, 3); // Check right return order + })); + + // While packets are sent in the correct order, rejection handlers + // registered in "Promise.jsm" may be invoked later than fulfillment + // handlers, meaning that we can't check the actual order with certainty. + let deferAfterRejection = promise.defer(); + + calls.push(rootClient.promiseThrow().then(() => { + do_check_true(false, "promiseThrow shouldn't succeed!"); + }, error => { + do_check_eq(sequence++, 4); // Check right return order + do_check_true(true, "simple throw should throw"); + deferAfterRejection.resolve(); + })); + + calls.push(rootClient.simpleReturn().then(ret => { + return deferAfterRejection.promise.then(function () { + do_check_eq(sequence, 5); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + }); + })); + + // Break up the backlog with a long request that waits + // for another simpleReturn before completing + calls.push(rootClient.promiseReturn(1).then(ret => { + return deferAfterRejection.promise.then(function () { + do_check_eq(sequence, 6); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + }); + })); + + calls.push(rootClient.simpleReturn().then(ret => { + return deferAfterRejection.promise.then(function () { + do_check_eq(sequence, 7); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + }); + })); + + promise.all(calls).then(() => { + client.close().then(() => { + do_test_finished(); + }); + }); + }); + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_protocol_children.js b/devtools/server/tests/unit/test_protocol_children.js new file mode 100644 index 000000000..67773ebef --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_children.js @@ -0,0 +1,559 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test simple requests using the protocol helpers. + */ +var protocol = require("devtools/shared/protocol"); +var {preEvent, types, Arg, Option, RetVal} = protocol; + +var events = require("sdk/event/core"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + }; +} + +var testTypes = {}; + +// Predeclaring the actor type so that it can be used in the +// implementation of the child actor. +types.addActorType("childActor"); + +const childSpec = protocol.generateActorSpec({ + typeName: "childActor", + + events: { + "event1" : { + a: Arg(0), + b: Arg(1), + c: Arg(2) + }, + "event2" : { + a: Arg(0), + b: Arg(1), + c: Arg(2) + }, + "named-event": { + type: "namedEvent", + a: Arg(0), + b: Arg(1), + c: Arg(2) + }, + "object-event": { + type: "objectEvent", + detail: Arg(0, "childActor#detail1"), + }, + "array-object-event": { + type: "arrayObjectEvent", + detail: Arg(0, "array:childActor#detail2"), + } + }, + + methods: { + echo: { + request: { str: Arg(0) }, + response: { str: RetVal("string") }, + }, + getDetail1: { + // This also exercises return-value-as-packet. + response: RetVal("childActor#detail1"), + }, + getDetail2: { + // This also exercises return-value-as-packet. + response: RetVal("childActor#detail2"), + }, + getIDDetail: { + response: { + idDetail: RetVal("childActor#actorid") + } + }, + getIntArray: { + request: { inputArray: Arg(0, "array:number") }, + response: RetVal("array:number") + }, + getSibling: { + request: { id: Arg(0) }, + response: { sibling: RetVal("childActor") } + }, + emitEvents: { + response: { value: "correct response" }, + }, + release: { + release: true + } + } +}); + +var ChildActor = protocol.ActorClassWithSpec(childSpec, { + // Actors returned by this actor should be owned by the root actor. + marshallPool: function () { return this.parent(); }, + + toString: function () { return "[ChildActor " + this.childID + "]"; }, + + initialize: function (conn, id) { + protocol.Actor.prototype.initialize.call(this, conn); + this.childID = id; + }, + + destroy: function () { + protocol.Actor.prototype.destroy.call(this); + this.destroyed = true; + }, + + form: function (detail) { + if (detail === "actorid") { + return this.actorID; + } + return { + actor: this.actorID, + childID: this.childID, + detail: detail + }; + }, + + echo: function (str) { + return str; + }, + + getDetail1: function () { + return this; + }, + + getDetail2: function () { + return this; + }, + + getIDDetail: function () { + return this; + }, + + getIntArray: function (inputArray) { + // Test that protocol.js converts an iterator to an array. + let f = function* () { + for (let i of inputArray) { + yield 2 * i; + } + }; + return f(); + }, + + getSibling: function (id) { + return this.parent().getChild(id); + }, + + emitEvents: function () { + events.emit(this, "event1", 1, 2, 3); + events.emit(this, "event2", 4, 5, 6); + events.emit(this, "named-event", 1, 2, 3); + events.emit(this, "object-event", this); + events.emit(this, "array-object-event", [this]); + }, + + release: function () { }, +}); + +var ChildFront = protocol.FrontClassWithSpec(childSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + }, + + destroy: function () { + this.destroyed = true; + protocol.Front.prototype.destroy.call(this); + }, + + marshallPool: function () { return this.parent(); }, + + toString: function () { return "[child front " + this.childID + "]"; }, + + form: function (form, detail) { + if (detail === "actorid") { + return; + } + this.childID = form.childID; + this.detail = form.detail; + }, + + onEvent1: preEvent("event1", function (a, b, c) { + this.event1arg3 = c; + }), + + onEvent2a: preEvent("event2", function (a, b, c) { + return promise.resolve().then(() => this.event2arg3 = c); + }), + + onEvent2b: preEvent("event2", function (a, b, c) { + this.event2arg2 = b; + }), +}); + +types.addDictType("manyChildrenDict", { + child5: "childActor", + more: "array:childActor", +}); + +types.addLifetime("temp", "_temporaryHolder"); + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + methods: { + getChild: { + request: { str: Arg(0) }, + response: { actor: RetVal("childActor") }, + }, + getChildren: { + request: { ids: Arg(0, "array:string") }, + response: { children: RetVal("array:childActor") }, + }, + getChildren2: { + request: { ids: Arg(0, "array:childActor") }, + response: { children: RetVal("array:childActor") }, + }, + getManyChildren: { + response: RetVal("manyChildrenDict") + }, + getTemporaryChild: { + request: { id: Arg(0) }, + response: { child: RetVal("temp:childActor") } + }, + clearTemporaryChildren: {} + } +}); + +var rootActor = null; +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + toString: function () { return "[root actor]"; }, + + initialize: function (conn) { + rootActor = this; + this.actorID = "root"; + this._children = {}; + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + }, + + sayHello: simpleHello, + + getChild: function (id) { + if (id in this._children) { + return this._children[id]; + } + let child = new ChildActor(this.conn, id); + this._children[id] = child; + return child; + }, + + getChildren: function (ids) { + return ids.map(id => this.getChild(id)); + }, + + getChildren2: function (ids) { + let f = function* () { + for (let c of ids) { + yield c; + } + }; + return f(); + }, + + getManyChildren: function () { + return { + foo: "bar", // note that this isn't in the specialization array. + child5: this.getChild("child5"), + more: [ this.getChild("child6"), this.getChild("child7") ] + }; + }, + + // This should remind you of a pause actor. + getTemporaryChild: function (id) { + if (!this._temporaryHolder) { + this._temporaryHolder = this.manage(new protocol.Actor(this.conn)); + } + return new ChildActor(this.conn, id); + }, + + clearTemporaryChildren: function (id) { + if (!this._temporaryHolder) { + return; + } + this._temporaryHolder.destroy(); + delete this._temporaryHolder; + } +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + toString: function () { return "[root front]"; }, + initialize: function (client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root actor owns itself. + this.manage(this); + }, + + getTemporaryChild: protocol.custom(function (id) { + if (!this._temporaryHolder) { + this._temporaryHolder = protocol.Front(this.conn); + this._temporaryHolder.actorID = this.actorID + "_temp"; + this._temporaryHolder = this.manage(this._temporaryHolder); + } + return this._getTemporaryChild(id); + }, { + impl: "_getTemporaryChild" + }), + + clearTemporaryChildren: protocol.custom(function () { + if (!this._temporaryHolder) { + return promise.resolve(undefined); + } + this._temporaryHolder.destroy(); + delete this._temporaryHolder; + return this._clearTemporaryChildren(); + }, { + impl: "_clearTemporaryChildren" + }) +}); + +function run_test() +{ + DebuggerServer.createRootActor = (conn => { + return RootActor(conn); + }); + DebuggerServer.init(); + + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + client.connect().then(([applicationType, traits]) => { + trace.expectReceive({"from":"", "applicationType":"xpcshell-tests", "traits":[]}); + do_check_eq(applicationType, "xpcshell-tests"); + + let rootFront = RootFront(client); + let childFront = null; + + let expectRootChildren = size => { + do_check_eq(rootActor._poolMap.size, size + 1); + do_check_eq(rootFront._poolMap.size, size + 1); + if (childFront) { + do_check_eq(childFront._poolMap.size, 0); + } + }; + + rootFront.getChild("child1").then(ret => { + trace.expectSend({"type":"getChild", "str":"child1", "to":""}); + trace.expectReceive({"actor":"", "from":""}); + + childFront = ret; + do_check_true(childFront instanceof ChildFront); + do_check_eq(childFront.childID, "child1"); + expectRootChildren(1); + }).then(() => { + // Request the child again, make sure the same is returned. + return rootFront.getChild("child1"); + }).then(ret => { + trace.expectSend({"type":"getChild", "str":"child1", "to":""}); + trace.expectReceive({"actor":"", "from":""}); + + expectRootChildren(1); + do_check_true(ret === childFront); + }).then(() => { + return childFront.echo("hello"); + }).then(ret => { + trace.expectSend({"type":"echo", "str":"hello", "to":""}); + trace.expectReceive({"str":"hello", "from":""}); + + do_check_eq(ret, "hello"); + }).then(() => { + return childFront.getDetail1(); + }).then(ret => { + trace.expectSend({"type":"getDetail1", "to":""}); + trace.expectReceive({"actor":"", "childID":"child1", "detail":"detail1", "from":""}); + do_check_true(ret === childFront); + do_check_eq(childFront.detail, "detail1"); + }).then(() => { + return childFront.getDetail2(); + }).then(ret => { + trace.expectSend({"type":"getDetail2", "to":""}); + trace.expectReceive({"actor":"", "childID":"child1", "detail":"detail2", "from":""}); + do_check_true(ret === childFront); + do_check_eq(childFront.detail, "detail2"); + }).then(() => { + return childFront.getIDDetail(); + }).then(ret => { + trace.expectSend({"type":"getIDDetail", "to":""}); + trace.expectReceive({"idDetail": childFront.actorID, "from":""}); + do_check_true(ret === childFront); + }).then(() => { + return childFront.getSibling("siblingID"); + }).then(ret => { + trace.expectSend({"type":"getSibling", "id":"siblingID", "to":""}); + trace.expectReceive({"sibling":{"actor":"", "childID":"siblingID"}, "from":""}); + + expectRootChildren(2); + }).then(ret => { + return rootFront.getTemporaryChild("temp1").then(temp1 => { + trace.expectSend({"type":"getTemporaryChild", "id":"temp1", "to":""}); + trace.expectReceive({"child":{"actor":"", "childID":"temp1"}, "from":""}); + + // At this point we expect two direct children, plus the temporary holder + // which should hold 1 itself. + do_check_eq(rootActor._temporaryHolder.__poolMap.size, 1); + do_check_eq(rootFront._temporaryHolder.__poolMap.size, 1); + + expectRootChildren(3); + return rootFront.getTemporaryChild("temp2").then(temp2 => { + trace.expectSend({"type":"getTemporaryChild", "id":"temp2", "to":""}); + trace.expectReceive({"child":{"actor":"", "childID":"temp2"}, "from":""}); + + // Same amount of direct children, and an extra in the temporary holder. + expectRootChildren(3); + do_check_eq(rootActor._temporaryHolder.__poolMap.size, 2); + do_check_eq(rootFront._temporaryHolder.__poolMap.size, 2); + + // Get the children of the temporary holder... + let checkActors = rootActor._temporaryHolder.__poolMap.values(); + let checkFronts = rootFront._temporaryHolder.__poolMap.values(); + + // Now release the temporary holders and expect them to drop again. + return rootFront.clearTemporaryChildren().then(() => { + trace.expectSend({"type":"clearTemporaryChildren", "to":""}); + trace.expectReceive({"from":""}); + + expectRootChildren(2); + do_check_false(!!rootActor._temporaryHolder); + do_check_false(!!rootFront._temporaryHolder); + for (let checkActor of checkActors) { + do_check_true(checkActor.destroyed); + do_check_true(checkActor.destroyed); + } + }); + }); + }); + }).then(ret => { + return rootFront.getChildren(["child1", "child2"]); + }).then(ret => { + trace.expectSend({"type":"getChildren", "ids":["child1", "child2"], "to":""}); + trace.expectReceive({"children":[{"actor":"", "childID":"child1"}, {"actor":"", "childID":"child2"}], "from":""}); + + expectRootChildren(3); + do_check_true(ret[0] === childFront); + do_check_true(ret[1] !== childFront); + do_check_true(ret[1] instanceof ChildFront); + + // On both children, listen to events. We're only + // going to trigger events on the first child, so an event + // triggered on the second should cause immediate failures. + + let set = new Set(["event1", "event2", "named-event", "object-event", "array-object-event"]); + + childFront.on("event1", (a, b, c) => { + do_check_eq(a, 1); + do_check_eq(b, 2); + do_check_eq(c, 3); + // Verify that the pre-event handler was called. + do_check_eq(childFront.event1arg3, 3); + set.delete("event1"); + }); + childFront.on("event2", (a, b, c) => { + do_check_eq(a, 4); + do_check_eq(b, 5); + do_check_eq(c, 6); + // Verify that the async pre-event handler was called, + // setting the property before this handler was called. + do_check_eq(childFront.event2arg3, 6); + // And check that the sync preEvent with the same name is also + // executed + do_check_eq(childFront.event2arg2, 5); + set.delete("event2"); + }); + childFront.on("named-event", (a, b, c) => { + do_check_eq(a, 1); + do_check_eq(b, 2); + do_check_eq(c, 3); + set.delete("named-event"); + }); + childFront.on("object-event", (obj) => { + do_check_true(obj === childFront); + do_check_eq(childFront.detail, "detail1"); + set.delete("object-event"); + }); + childFront.on("array-object-event", (array) => { + do_check_true(array[0] === childFront); + do_check_eq(childFront.detail, "detail2"); + set.delete("array-object-event"); + }); + + let fail = function () { + do_throw("Unexpected event"); + }; + ret[1].on("event1", fail); + ret[1].on("event2", fail); + ret[1].on("named-event", fail); + ret[1].on("object-event", fail); + ret[1].on("array-object-event", fail); + + return childFront.emitEvents().then(() => { + trace.expectSend({"type":"emitEvents", "to":""}); + trace.expectReceive({"type":"event1", "a":1, "b":2, "c":3, "from":""}); + trace.expectReceive({"type":"event2", "a":4, "b":5, "c":6, "from":""}); + trace.expectReceive({"type":"namedEvent", "a":1, "b":2, "c":3, "from":""}); + trace.expectReceive({"type":"objectEvent", "detail":{"actor":"", "childID":"child1", "detail":"detail1"}, "from":""}); + trace.expectReceive({"type":"arrayObjectEvent", "detail":[{"actor":"", "childID":"child1", "detail":"detail2"}], "from":""}); + trace.expectReceive({"value":"correct response", "from":""}); + + + do_check_eq(set.size, 0); + }); + }).then(ret => { + return rootFront.getManyChildren(); + }).then(ret => { + trace.expectSend({"type":"getManyChildren", "to":""}); + trace.expectReceive({"foo":"bar", "child5":{"actor":"", "childID":"child5"}, "more":[{"actor":"", "childID":"child6"}, {"actor":"", "childID":"child7"}], "from":""}); + + // Check all the crazy stuff we did in getManyChildren + do_check_eq(ret.foo, "bar"); + do_check_eq(ret.child5.childID, "child5"); + do_check_eq(ret.more[0].childID, "child6"); + do_check_eq(ret.more[1].childID, "child7"); + }).then(() => { + // Test accepting a generator. + let f = function* () { + for (let i of [1, 2, 3, 4, 5]) { + yield i; + } + }; + return childFront.getIntArray(f()); + }).then((ret) => { + do_check_eq(ret.length, 5); + let expected = [2, 4, 6, 8, 10]; + for (let i = 0; i < 5; ++i) { + do_check_eq(ret[i], expected[i]); + } + }).then(() => { + return rootFront.getChildren(["child1", "child2"]); + }).then(ids => { + let f = function* () { + for (let id of ids) { + yield id; + } + }; + return rootFront.getChildren2(f()); + }).then(ret => { + do_check_eq(ret.length, 2); + do_check_true(ret[0] === childFront); + do_check_true(ret[1] !== childFront); + do_check_true(ret[1] instanceof ChildFront); + }).then(() => { + client.close().then(() => { + do_test_finished(); + }); + }).then(null, err => { + do_report_unexpected_exception(err, "Failure executing test"); + }); + }); + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_protocol_formtype.js b/devtools/server/tests/unit/test_protocol_formtype.js new file mode 100644 index 000000000..27ac0bee9 --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_formtype.js @@ -0,0 +1,177 @@ +var protocol = require("devtools/shared/protocol"); +var {Arg, Option, RetVal} = protocol; + +protocol.types.addActorType("child"); +protocol.types.addActorType("root"); + +const childSpec = protocol.generateActorSpec({ + typeName: "child", + + methods: { + getChild: { + response: RetVal("child") + } + } +}); + +// The child actor doesn't provide a form description +var ChildActor = protocol.ActorClassWithSpec(childSpec, { + initialize(conn) { + protocol.Actor.prototype.initialize.call(this, conn); + }, + + form(detail) { + return { + actor: this.actorID, + extra: "extra" + }; + }, + + getChild: function () { + return this; + } +}); + +var ChildFront = protocol.FrontClassWithSpec(childSpec, { + initialize(client) { + protocol.Front.prototype.initialize.call(this, client); + }, + + form(v, ctx, detail) { + this.extra = v.extra; + } +}); + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + // Basic form type, relies on implicit DictType creation + formType: { + childActor: "child" + }, + + // This detail uses explicit DictType creation + "formType#detail1": protocol.types.addDictType("RootActorFormTypeDetail1", { + detailItem: "child" + }), + + // This detail a string type. + "formType#actorid": "string", + + methods: { + getDefault: { + response: RetVal("root") + }, + getDetail1: { + response: RetVal("root#detail1") + }, + getDetail2: { + response: { + item: RetVal("root#actorid") + } + }, + getUnknownDetail: { + response: RetVal("root#unknownDetail") + } + } +}); + +// The root actor does provide a form description. +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + initialize(conn) { + protocol.Actor.prototype.initialize.call(this, conn); + this.manage(this); + this.child = new ChildActor(); + }, + + sayHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [] + }; + }, + + form(detail) { + if (detail === "detail1") { + return { + actor: this.actorID, + detailItem: this.child + }; + } else if (detail === "actorid") { + return this.actorID; + } + + return { + actor: this.actorID, + childActor: this.child + }; + }, + + getDefault: function () { + return this; + }, + + getDetail1: function () { + return this; + }, + + getDetail2: function () { + return this; + }, + + getUnknownDetail: function () { + return this; + } +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + initialize(client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + + // Root owns itself. + this.manage(this); + }, + + form(v, ctx, detail) { + this.lastForm = v; + } +}); + +const run_test = Test(function* () { + DebuggerServer.createRootActor = (conn => { + return RootActor(conn); + }); + DebuggerServer.init(); + + const connection = DebuggerServer.connectPipe(); + const conn = new DebuggerClient(connection); + const client = Async(conn); + + yield client.connect(); + + let rootFront = RootFront(conn); + + // Trigger some methods that return forms. + let retval = yield rootFront.getDefault(); + do_check_true(retval instanceof RootFront); + do_check_true(rootFront.lastForm.childActor instanceof ChildFront); + + retval = yield rootFront.getDetail1(); + do_check_true(retval instanceof RootFront); + do_check_true(rootFront.lastForm.detailItem instanceof ChildFront); + + retval = yield rootFront.getDetail2(); + do_check_true(retval instanceof RootFront); + do_check_true(typeof (rootFront.lastForm) === "string"); + + // getUnknownDetail should fail, since no typeName is specified. + try { + yield rootFront.getUnknownDetail(); + do_check_true(false); + } catch (ex) { + } + + yield client.close(); +}); diff --git a/devtools/server/tests/unit/test_protocol_longstring.js b/devtools/server/tests/unit/test_protocol_longstring.js new file mode 100644 index 000000000..c37f4251e --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_longstring.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test simple requests using the protocol helpers. + */ +var protocol = require("devtools/shared/protocol"); +var {RetVal, Arg, Option} = protocol; +var events = require("sdk/event/core"); +var {LongStringActor} = require("devtools/server/actors/string"); + +// The test implicitly relies on this. +require("devtools/shared/fronts/string"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + }; +} + +DebuggerServer.LONG_STRING_LENGTH = DebuggerServer.LONG_STRING_INITIAL_LENGTH = DebuggerServer.LONG_STRING_READ_LENGTH = 5; + +var SHORT_STR = "abc"; +var LONG_STR = "abcdefghijklmnop"; + +var rootActor = null; + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + events: { + "string-event": { + str: Arg(0, "longstring") + } + }, + + methods: { + shortString: { + response: { value: RetVal("longstring") }, + }, + longString: { + response: { value: RetVal("longstring") }, + }, + emitShortString: { + oneway: true, + }, + emitLongString: { + oneway: true, + } + } +}); + +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + initialize: function (conn) { + rootActor = this; + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + this.actorID = "root"; + }, + + sayHello: simpleHello, + + shortString: function () { + return new LongStringActor(this.conn, SHORT_STR); + }, + + longString: function () { + return new LongStringActor(this.conn, LONG_STR); + }, + + emitShortString: function () { + events.emit(this, "string-event", new LongStringActor(this.conn, SHORT_STR)); + }, + + emitLongString: function () { + events.emit(this, "string-event", new LongStringActor(this.conn, LONG_STR)); + }, +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + initialize: function (client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root owns itself. + this.manage(this); + } +}); + +function run_test() +{ + DebuggerServer.createRootActor = (conn => { + return RootActor(conn); + }); + + DebuggerServer.init(); + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + let rootClient; + + let strfront = null; + + let expectRootChildren = function (size) { + do_check_eq(rootActor.__poolMap.size, size + 1); + do_check_eq(rootClient.__poolMap.size, size + 1); + }; + + + client.connect().then(([applicationType, traits]) => { + rootClient = RootFront(client); + + // Root actor has no children yet. + expectRootChildren(0); + + trace.expectReceive({"from":"", "applicationType":"xpcshell-tests", "traits":[]}); + do_check_eq(applicationType, "xpcshell-tests"); + rootClient.shortString().then(ret => { + trace.expectSend({"type":"shortString", "to":""}); + trace.expectReceive({"value":"abc", "from":""}); + + // Should only own the one reference (itself) at this point. + expectRootChildren(0); + strfront = ret; + }).then(() => { + return strfront.string(); + }).then(ret => { + do_check_eq(ret, SHORT_STR); + }).then(() => { + return rootClient.longString(); + }).then(ret => { + trace.expectSend({"type":"longString", "to":""}); + trace.expectReceive({"value":{"type":"longString", "actor":"", "length":16, "initial":"abcde"}, "from":""}); + + strfront = ret; + // Should own a reference to itself and an extra string now. + expectRootChildren(1); + }).then(() => { + return strfront.string(); + }).then(ret => { + trace.expectSend({"type":"substring", "start":5, "end":10, "to":""}); + trace.expectReceive({"substring":"fghij", "from":""}); + trace.expectSend({"type":"substring", "start":10, "end":15, "to":""}); + trace.expectReceive({"substring":"klmno", "from":""}); + trace.expectSend({"type":"substring", "start":15, "end":20, "to":""}); + trace.expectReceive({"substring":"p", "from":""}); + + do_check_eq(ret, LONG_STR); + }).then(() => { + return strfront.release(); + }).then(() => { + trace.expectSend({"type":"release", "to":""}); + trace.expectReceive({"from":""}); + + // That reference should be removed now. + expectRootChildren(0); + }).then(() => { + let deferred = promise.defer(); + rootClient.once("string-event", (str) => { + trace.expectSend({"type":"emitShortString", "to":""}); + trace.expectReceive({"type":"string-event", "str":"abc", "from":""}); + + do_check_true(!!str); + strfront = str; + // Shouldn't generate any new references + expectRootChildren(0); + // will generate no packets. + strfront.string().then((value) => { deferred.resolve(value); }); + }); + rootClient.emitShortString(); + return deferred.promise; + }).then(value => { + do_check_eq(value, SHORT_STR); + }).then(() => { + // Will generate no packets + return strfront.release(); + }).then(() => { + let deferred = promise.defer(); + rootClient.once("string-event", (str) => { + trace.expectSend({"type":"emitLongString", "to":""}); + trace.expectReceive({"type":"string-event", "str":{"type":"longString", "actor":"", "length":16, "initial":"abcde"}, "from":""}); + + do_check_true(!!str); + // Should generate one new reference + expectRootChildren(1); + strfront = str; + strfront.string().then((value) => { + trace.expectSend({"type":"substring", "start":5, "end":10, "to":""}); + trace.expectReceive({"substring":"fghij", "from":""}); + trace.expectSend({"type":"substring", "start":10, "end":15, "to":""}); + trace.expectReceive({"substring":"klmno", "from":""}); + trace.expectSend({"type":"substring", "start":15, "end":20, "to":""}); + trace.expectReceive({"substring":"p", "from":""}); + + deferred.resolve(value); + }); + }); + rootClient.emitLongString(); + return deferred.promise; + }).then(value => { + do_check_eq(value, LONG_STR); + }).then(() => { + return strfront.release(); + }).then(() => { + trace.expectSend({"type":"release", "to":""}); + trace.expectReceive({"from":""}); + expectRootChildren(0); + }).then(() => { + client.close().then(() => { + do_test_finished(); + }); + }).then(null, err => { + do_report_unexpected_exception(err, "Failure executing test"); + }); + }); + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_protocol_simple.js b/devtools/server/tests/unit/test_protocol_simple.js new file mode 100644 index 000000000..c85003954 --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_simple.js @@ -0,0 +1,319 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test simple requests using the protocol helpers. + */ + +var protocol = require("devtools/shared/protocol"); +var {Arg, Option, RetVal} = protocol; +var events = require("sdk/event/core"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + }; +} + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + events: { + "oneway": { a: Arg(0) }, + "falsyOptions": { + zero: Option(0), + farce: Option(0) + } + }, + + methods: { + simpleReturn: { + response: { value: RetVal() }, + }, + promiseReturn: { + response: { value: RetVal("number") }, + }, + simpleArgs: { + request: { + firstArg: Arg(0), + secondArg: Arg(1), + }, + response: RetVal() + }, + nestedArgs: { + request: { + firstArg: Arg(0), + nest: { + secondArg: Arg(1), + nest: { + thirdArg: Arg(2) + } + } + }, + response: RetVal() + }, + optionArgs: { + request: { + option1: Option(0), + option2: Option(0) + }, + response: RetVal() + }, + optionalArgs: { + request: { + a: Arg(0), + b: Arg(1, "nullable:number") + }, + response: { + value: RetVal("number") + }, + }, + arrayArgs: { + request: { + a: Arg(0, "array:number") + }, + response: { + arrayReturn: RetVal("array:number") + }, + }, + nestedArrayArgs: { + request: { a: Arg(0, "array:array:number") }, + response: { value: RetVal("array:array:number") }, + }, + renamedEcho: { + request: { + type: "echo", + a: Arg(0), + }, + response: { + value: RetVal("string") + }, + }, + testOneWay: { + request: { a: Arg(0) }, + oneway: true + }, + emitFalsyOptions: { + oneway: true + } + } +}); + +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + initialize: function (conn) { + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + this.actorID = "root"; + }, + + sayHello: simpleHello, + + simpleReturn: function () { + return 1; + }, + + promiseReturn: function () { + return promise.resolve(1); + }, + + simpleArgs: function (a, b) { + return { firstResponse: a + 1, secondResponse: b + 1 }; + }, + + nestedArgs: function (a, b, c) { + return { a: a, b: b, c: c }; + }, + + optionArgs: function (options) { + return { option1: options.option1, option2: options.option2 }; + }, + + optionalArgs: function (a, b = 200) { + return b; + }, + + arrayArgs: function (a) { + return a; + }, + + nestedArrayArgs: function (a) { + return a; + }, + + /** + * Test that the 'type' part of the request packet works + * correctly when the type isn't the same as the method name + */ + renamedEcho: function (a) { + if (this.conn.currentPacket.type != "echo") { + return "goodbye"; + } + return a; + }, + + testOneWay: function (a) { + // Emit to show that we got this message, because there won't be a response. + events.emit(this, "oneway", a); + }, + + emitFalsyOptions: function () { + events.emit(this, "falsyOptions", { zero: 0, farce: false }); + } +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + initialize: function (client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root owns itself. + this.manage(this); + } +}); + +function run_test() +{ + DebuggerServer.createRootActor = (conn => { + return RootActor(conn); + }); + DebuggerServer.init(); + + check_except(() => { + let badActor = ActorClassWithSpec({}, { + missing: preEvent("missing-event", function () { + }) + }); + }); + + protocol.types.getType("array:array:array:number"); + protocol.types.getType("array:array:array:number"); + + check_except(() => protocol.types.getType("unknown")); + check_except(() => protocol.types.getType("array:unknown")); + check_except(() => protocol.types.getType("unknown:number")); + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + let rootClient; + + client.connect().then(([applicationType, traits]) => { + trace.expectReceive({"from":"", "applicationType":"xpcshell-tests", "traits":[]}); + do_check_eq(applicationType, "xpcshell-tests"); + + rootClient = RootFront(client); + + rootClient.simpleReturn().then(ret => { + trace.expectSend({"type":"simpleReturn", "to":""}); + trace.expectReceive({"value":1, "from":""}); + do_check_eq(ret, 1); + }).then(() => { + return rootClient.promiseReturn(); + }).then(ret => { + trace.expectSend({"type":"promiseReturn", "to":""}); + trace.expectReceive({"value":1, "from":""}); + do_check_eq(ret, 1); + }).then(() => { + // Missing argument should throw an exception + check_except(() => { + rootClient.simpleArgs(5); + }); + + return rootClient.simpleArgs(5, 10); + }).then(ret => { + trace.expectSend({"type":"simpleArgs", "firstArg":5, "secondArg":10, "to":""}); + trace.expectReceive({"firstResponse":6, "secondResponse":11, "from":""}); + do_check_eq(ret.firstResponse, 6); + do_check_eq(ret.secondResponse, 11); + }).then(() => { + return rootClient.nestedArgs(1, 2, 3); + }).then(ret => { + trace.expectSend({"type":"nestedArgs", "firstArg":1, "nest":{"secondArg":2, "nest":{"thirdArg":3}}, "to":""}); + trace.expectReceive({"a":1, "b":2, "c":3, "from":""}); + do_check_eq(ret.a, 1); + do_check_eq(ret.b, 2); + do_check_eq(ret.c, 3); + }).then(() => { + return rootClient.optionArgs({ + "option1": 5, + "option2": 10 + }); + }).then(ret => { + trace.expectSend({"type":"optionArgs", "option1":5, "option2":10, "to":""}); + trace.expectReceive({"option1":5, "option2":10, "from":""}); + do_check_eq(ret.option1, 5); + do_check_eq(ret.option2, 10); + }).then(() => { + return rootClient.optionArgs({}); + }).then(ret => { + trace.expectSend({"type":"optionArgs", "to":""}); + trace.expectReceive({"from":""}); + do_check_true(typeof (ret.option1) === "undefined"); + do_check_true(typeof (ret.option2) === "undefined"); + }).then(() => { + // Explicitly call an optional argument... + return rootClient.optionalArgs(5, 10); + }).then(ret => { + trace.expectSend({"type":"optionalArgs", "a":5, "b":10, "to":""}); + trace.expectReceive({"value":10, "from":""}); + do_check_eq(ret, 10); + }).then(() => { + // Now don't pass the optional argument, expect the default. + return rootClient.optionalArgs(5); + }).then(ret => { + trace.expectSend({"type":"optionalArgs", "a":5, "to":""}); + trace.expectReceive({"value":200, "from":""}); + do_check_eq(ret, 200); + }).then(ret => { + return rootClient.arrayArgs([0, 1, 2, 3, 4, 5]); + }).then(ret => { + trace.expectSend({"type":"arrayArgs", "a":[0, 1, 2, 3, 4, 5], "to":""}); + trace.expectReceive({"arrayReturn":[0, 1, 2, 3, 4, 5], "from":""}); + do_check_eq(ret[0], 0); + do_check_eq(ret[5], 5); + }).then(() => { + return rootClient.arrayArgs([[5]]); + }).then(ret => { + trace.expectSend({"type":"arrayArgs", "a":[[5]], "to":""}); + trace.expectReceive({"arrayReturn":[[5]], "from":""}); + do_check_eq(ret[0][0], 5); + }).then(() => { + return rootClient.renamedEcho("hello"); + }).then(str => { + trace.expectSend({"type":"echo", "a":"hello", "to":""}); + trace.expectReceive({"value":"hello", "from":""}); + + do_check_eq(str, "hello"); + + let deferred = promise.defer(); + rootClient.on("oneway", (response) => { + trace.expectSend({"type":"testOneWay", "a":"hello", "to":""}); + trace.expectReceive({"type":"oneway", "a":"hello", "from":""}); + + do_check_eq(response, "hello"); + deferred.resolve(); + }); + do_check_true(typeof (rootClient.testOneWay("hello")) === "undefined"); + return deferred.promise; + }).then(() => { + let deferred = promise.defer(); + rootClient.on("falsyOptions", res => { + trace.expectSend({"type":"emitFalsyOptions", "to":""}); + trace.expectReceive({"type":"falsyOptions", "farce":false, "zero": 0, "from":""}); + + do_check_true(res.zero === 0); + do_check_true(res.farce === false); + deferred.resolve(); + }); + rootClient.emitFalsyOptions(); + return deferred.promise; + }).then(() => { + client.close().then(() => { + do_test_finished(); + }); + }).then(null, err => { + do_report_unexpected_exception(err, "Failure executing test"); + }); + }); + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_protocol_stack.js b/devtools/server/tests/unit/test_protocol_stack.js new file mode 100644 index 000000000..a81f99a8e --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_stack.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Client request stacks should span the entire process from before making the + * request to handling the reply from the server. The server frames are not + * included, nor can they be in most cases, since the server can be a remote + * device. + */ + +var protocol = require("devtools/shared/protocol"); +var {Arg, Option, RetVal} = protocol; +var events = require("sdk/event/core"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + }; +} + +const rootSpec = protocol.generateActorSpec({ + typeName: "root", + + methods: { + simpleReturn: { + response: { value: RetVal() }, + } + } +}); + +var RootActor = protocol.ActorClassWithSpec(rootSpec, { + initialize: function (conn) { + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + this.actorID = "root"; + this.sequence = 0; + }, + + sayHello: simpleHello, + + simpleReturn: function () { + return this.sequence++; + } +}); + +var RootFront = protocol.FrontClassWithSpec(rootSpec, { + initialize: function (client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root owns itself. + this.manage(this); + } +}); + +function run_test() { + if (!Services.prefs.getBoolPref("javascript.options.asyncstack")) { + do_print("Async stacks are disabled."); + return; + } + + DebuggerServer.createRootActor = RootActor; + DebuggerServer.init(); + + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + let rootClient; + + client.connect().then(function onConnect() { + rootClient = RootFront(client); + + rootClient.simpleReturn().then(() => { + let stack = Components.stack; + while (stack) { + do_print(stack.name); + if (stack.name == "onConnect") { + // Reached back to outer function before request + ok(true, "Complete stack"); + return; + } + stack = stack.asyncCaller || stack.caller; + } + ok(false, "Incomplete stack"); + }, () => { + ok(false, "Request failed unexpectedly"); + }).then(() => { + client.close().then(() => { + do_test_finished(); + }); + }); + }); + + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_protocol_unregister.js b/devtools/server/tests/unit/test_protocol_unregister.js new file mode 100644 index 000000000..5b32dd0a3 --- /dev/null +++ b/devtools/server/tests/unit/test_protocol_unregister.js @@ -0,0 +1,44 @@ +const {types} = require("devtools/shared/protocol"); + + +function run_test() +{ + types.addType("test", { + read: (v) => { return "successful read: " + v; }, + write: (v) => { return "successful write: " + v; } + }); + + // Verify the type registered correctly. + + let type = types.getType("test"); + let arrayType = types.getType("array:test"); + do_check_eq(type.read("foo"), "successful read: foo"); + do_check_eq(arrayType.read(["foo"])[0], "successful read: foo"); + + types.removeType("test"); + + do_check_eq(type.name, "DEFUNCT:test"); + try { + types.getType("test"); + do_check_true(false, "getType should fail"); + } catch (ex) { + do_check_eq(ex.toString(), "Error: Unknown type: test"); + } + + try { + type.read("foo"); + do_check_true(false, "type.read should have thrown an exception."); + } catch (ex) { + do_check_eq(ex.toString(), "Error: Using defunct type: test"); + } + + try { + arrayType.read(["foo"]); + do_check_true(false, "array:test.read should have thrown an exception."); + } catch (ex) { + do_check_eq(ex.toString(), "Error: Using defunct type: test"); + } + +} + + diff --git a/devtools/server/tests/unit/test_reattach-thread.js b/devtools/server/tests/unit/test_reattach-thread.js new file mode 100644 index 000000000..6d089e896 --- /dev/null +++ b/devtools/server/tests/unit/test_reattach-thread.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that reattaching to a previously detached thread works. + */ + +var gClient, gDebuggee, gThreadClient, gTabClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = testGlobal("test-reattach"); + DebuggerServer.addTestGlobal(gDebuggee); + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect().then(() => { + attachTestTab(gClient, "test-reattach", (aReply, aTabClient) => { + gTabClient = aTabClient; + test_attach(); + }); + }); + do_test_pending(); +} + +function test_attach() +{ + gTabClient.attachThread({}, (aResponse, aThreadClient) => { + do_check_eq(aThreadClient.state, "paused"); + gThreadClient = aThreadClient; + aThreadClient.resume(test_detach); + }); +} + +function test_detach() +{ + gThreadClient.detach(() => { + do_check_eq(gThreadClient.state, "detached"); + do_check_eq(gTabClient.thread, null); + test_reattach(); + }); +} + +function test_reattach() +{ + gTabClient.attachThread({}, (aResponse, aThreadClient) => { + do_check_neq(gThreadClient, aThreadClient); + do_check_eq(aThreadClient.state, "paused"); + do_check_eq(gTabClient.thread, aThreadClient); + aThreadClient.resume(cleanup); + }); +} + +function cleanup() +{ + gClient.close().then(do_test_finished); +} diff --git a/devtools/server/tests/unit/test_registerClient.js b/devtools/server/tests/unit/test_registerClient.js new file mode 100644 index 000000000..c018e4454 --- /dev/null +++ b/devtools/server/tests/unit/test_registerClient.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the DebuggerClient.registerClient API + +var EventEmitter = require("devtools/shared/event-emitter"); + +var gClient; +var gActors; +var gTestClient; + +function TestActor(conn) { + this.conn = conn; +} +TestActor.prototype = { + actorPrefix: "test", + + start: function () { + this.conn.sendActorEvent(this.actorID, "foo", { + hello: "world" + }); + return {}; + } +}; +TestActor.prototype.requestTypes = { + "start": TestActor.prototype.start +}; + +function TestClient(client, form) { + this.client = client; + this.actor = form.test; + this.events = ["foo"]; + EventEmitter.decorate(this); + client.registerClient(this); + + this.detached = false; +} +TestClient.prototype = { + start: function () { + this.client.request({ + to: this.actor, + type: "start" + }); + }, + + detach: function (onDone) { + this.detached = true; + onDone(); + } +}; + +function run_test() +{ + DebuggerServer.addGlobalActor(TestActor); + + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + add_test(init); + add_test(test_client_events); + add_test(close_client); + run_next_test(); +} + +function init() +{ + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect() + .then(() => gClient.listTabs()) + .then(aResponse => { + gActors = aResponse; + gTestClient = new TestClient(gClient, aResponse); + run_next_test(); + }); +} + +function test_client_events() +{ + // Test DebuggerClient.registerClient and DebuggerServerConnection.sendActorEvent + gTestClient.on("foo", function (type, data) { + do_check_eq(type, "foo"); + do_check_eq(data.hello, "world"); + run_next_test(); + }); + gTestClient.start(); +} + +function close_client() { + gClient.close().then(() => { + // Check that client.detach method is call on client destruction + do_check_true(gTestClient.detached); + run_next_test(); + }); +} + diff --git a/devtools/server/tests/unit/test_register_actor.js b/devtools/server/tests/unit/test_register_actor.js new file mode 100644 index 000000000..8f3a243eb --- /dev/null +++ b/devtools/server/tests/unit/test_register_actor.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); + +function check_actors(expect) { + do_check_eq(expect, DebuggerServer.tabActorFactories.hasOwnProperty("registeredActor1")); + do_check_eq(expect, DebuggerServer.tabActorFactories.hasOwnProperty("registeredActor2")); + + do_check_eq(expect, DebuggerServer.globalActorFactories.hasOwnProperty("registeredActor2")); + do_check_eq(expect, DebuggerServer.globalActorFactories.hasOwnProperty("registeredActor1")); +} + +function run_test() +{ + // Allow incoming connections. + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + add_test(test_deprecated_api); + add_test(test_lazy_api); + add_test(cleanup); + run_next_test(); +} + +function test_deprecated_api() { + // The xpcshell-test/ path maps to resource://test/ + DebuggerServer.registerModule("xpcshell-test/registertestactors-01"); + DebuggerServer.registerModule("xpcshell-test/registertestactors-02"); + + check_actors(true); + + check_except(() => { + DebuggerServer.registerModule("xpcshell-test/registertestactors-01"); + }); + check_except(() => { + DebuggerServer.registerModule("xpcshell-test/registertestactors-02"); + }); + + DebuggerServer.unregisterModule("xpcshell-test/registertestactors-01"); + DebuggerServer.unregisterModule("xpcshell-test/registertestactors-02"); + check_actors(false); + + DebuggerServer.registerModule("xpcshell-test/registertestactors-01"); + DebuggerServer.registerModule("xpcshell-test/registertestactors-02"); + check_actors(true); + + run_next_test(); +} + +// Bug 988237: Test the new lazy actor loading +function test_lazy_api() { + let isActorLoaded = false; + let isActorInstanciated = false; + function onActorEvent(subject, topic, data) { + if (data == "loaded") { + isActorLoaded = true; + } else if (data == "instantiated") { + isActorInstanciated = true; + } + } + Services.obs.addObserver(onActorEvent, "actor", false); + DebuggerServer.registerModule("xpcshell-test/registertestactors-03", { + prefix: "lazy", + constructor: "LazyActor", + type: { global: true, tab: true } + }); + // The actor is immediatly registered, but not loaded + do_check_true(DebuggerServer.tabActorFactories.hasOwnProperty("lazyActor")); + do_check_true(DebuggerServer.globalActorFactories.hasOwnProperty("lazyActor")); + do_check_false(isActorLoaded); + do_check_false(isActorInstanciated); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect().then(function onConnect() { + client.listTabs(onListTabs); + }); + function onListTabs(aResponse) { + // On listTabs, the actor is still not loaded, + // but we can see its name in the list of available actors + do_check_false(isActorLoaded); + do_check_false(isActorInstanciated); + do_check_true("lazyActor" in aResponse); + + let {LazyFront} = require("xpcshell-test/registertestactors-03"); + let front = LazyFront(client, aResponse); + front.hello().then(onRequest); + } + function onRequest(aResponse) { + do_check_eq(aResponse, "world"); + + // Finally, the actor is loaded on the first request being made to it + do_check_true(isActorLoaded); + do_check_true(isActorInstanciated); + + Services.obs.removeObserver(onActorEvent, "actor", false); + client.close().then(() => run_next_test()); + } +} + +function cleanup() { + DebuggerServer.destroy(); + + // Check that all actors are unregistered on server destruction + check_actors(false); + do_check_false(DebuggerServer.tabActorFactories.hasOwnProperty("lazyActor")); + do_check_false(DebuggerServer.globalActorFactories.hasOwnProperty("lazyActor")); + + run_next_test(); +} + diff --git a/devtools/server/tests/unit/test_requestTypes.js b/devtools/server/tests/unit/test_requestTypes.js new file mode 100644 index 000000000..694e276bc --- /dev/null +++ b/devtools/server/tests/unit/test_requestTypes.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { RootActor } = require("devtools/server/actors/root"); + +function test_requestTypes_request(aClient, anActor) +{ + aClient.request({ to: "root", type: "requestTypes" }, function (aResponse) { + var expectedRequestTypes = Object.keys(RootActor. + prototype. + requestTypes); + + do_check_true(Array.isArray(aResponse.requestTypes)); + do_check_eq(JSON.stringify(aResponse.requestTypes), + JSON.stringify(expectedRequestTypes)); + + aClient.close().then(() => { + do_test_finished(); + }); + }); +} + +function run_test() +{ + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + var client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect().then(function () { + test_requestTypes_request(client); + }); + + do_test_pending(); +} diff --git a/devtools/server/tests/unit/test_safe-getter.js b/devtools/server/tests/unit/test_safe-getter.js new file mode 100644 index 000000000..08949154d --- /dev/null +++ b/devtools/server/tests/unit/test_safe-getter.js @@ -0,0 +1,25 @@ +function run_test() { + Components.utils.import("resource://gre/modules/jsdebugger.jsm"); + addDebuggerToGlobal(this); + var g = testGlobal("test"); + var dbg = new Debugger(); + var gw = dbg.addDebuggee(g); + + g.eval(` + // This is not a CCW. + Object.defineProperty(this, "bar", { + get: function() { return "bar"; }, + configurable: true, + enumerable: true + }); + + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + + // This is a CCW. + XPCOMUtils.defineLazyGetter(this, "foo", function() { return "foo"; }); + `); + + // Neither scripted getter should be considered safe. + assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("bar"))); + assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("foo"))); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/unit/test_setBreakpoint-on-column-in-gcd-script.js new file mode 100644 index 000000000..a9d82e434 --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-column-in-gcd-script.js @@ -0,0 +1,58 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-column-in-gcd-script.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + let global = testGlobal("test"); + loadSubScript(SOURCE_URL, global); + Cu.forceGC(); Cu.forceGC(); Cu.forceGC(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + DebuggerServer.addTestGlobal(global); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let { sources } = yield getSources(threadClient); + let source = findSource(sources, SOURCE_URL); + let sourceClient = threadClient.source(source); + + let location = { line: 6, column: 17 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_true(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + reload(tabClient).then(function () { + loadSubScript(SOURCE_URL, global); + }); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + do_check_eq(where.column, location.column); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value, 1); + do_check_eq(variables.b.value.type, "undefined"); + do_check_eq(variables.c.value.type, "undefined"); + yield resume(threadClient); + + yield close(client); + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-column-with-no-offsets-at-end-of-line.js b/devtools/server/tests/unit/test_setBreakpoint-on-column-with-no-offsets-at-end-of-line.js new file mode 100644 index 000000000..6185cf589 --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-column-with-no-offsets-at-end-of-line.js @@ -0,0 +1,39 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-column-with-no-offsets-at-end-of-line.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + + let global = createTestGlobal("test"); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let promise = waitForNewSource(threadClient, SOURCE_URL); + loadSubScript(SOURCE_URL, global); + let { source } = yield promise; + let sourceClient = threadClient.source(source); + + let location = { line: 4, column: 23 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_true(packet.isPending); + do_check_false("actualLocation" in packet); + + Cu.evalInSandbox("f()", global); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-column.js b/devtools/server/tests/unit/test_setBreakpoint-on-column.js new file mode 100644 index 000000000..bee9fe004 --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-column.js @@ -0,0 +1,57 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-column.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + + let global = createTestGlobal("test"); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let promise = waitForNewSource(threadClient, SOURCE_URL); + loadSubScript(SOURCE_URL, global); + let { source } = yield promise; + let sourceClient = threadClient.source(source); + + let location = { line: 4, column: 17 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_false(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", global); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + do_check_eq(where.column, location.column); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value, 1); + do_check_eq(variables.b.value.type, "undefined"); + do_check_eq(variables.c.value.type, "undefined"); + + yield resume(threadClient); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/unit/test_setBreakpoint-on-line-in-gcd-script.js new file mode 100644 index 000000000..8479c797e --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-in-gcd-script.js @@ -0,0 +1,57 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-line-in-gcd-script.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + let global = createTestGlobal("test"); + loadSubScript(SOURCE_URL, global); + Cu.forceGC(); Cu.forceGC(); Cu.forceGC(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + DebuggerServer.addTestGlobal(global); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let { sources } = yield getSources(threadClient); + let source = findSource(sources, SOURCE_URL); + let sourceClient = threadClient.source(source); + + let location = { line: 7 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_true(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + reload(tabClient).then(function () { + loadSubScript(SOURCE_URL, global); + }); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value, 1); + do_check_eq(variables.b.value.type, "undefined"); + do_check_eq(variables.c.value.type, "undefined"); + yield resume(threadClient); + + yield close(client); + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-offsets.js new file mode 100644 index 000000000..2f5c1b9aa --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-offsets.js @@ -0,0 +1,70 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-multiple-offsets.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + + let global = createTestGlobal("test"); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let promise = waitForNewSource(threadClient, SOURCE_URL); + loadSubScript(SOURCE_URL, global); + let { source } = yield promise; + let sourceClient = threadClient.source(source); + + let location = { line: 4 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_false(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", global); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.i.value.type, "undefined"); + + packet = yield executeOnNextTickAndWaitForPause(function () { + resume(threadClient); + }, client); + do_check_eq(packet.type, "paused"); + why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + frame = packet.frame; + where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + variables = frame.environment.bindings.variables; + do_check_eq(variables.i.value, 0); + + yield resume(threadClient); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-statements.js new file mode 100644 index 000000000..104152441 --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-multiple-statements.js @@ -0,0 +1,57 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-multiple-statements.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + + let global = createTestGlobal("test"); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let promise = waitForNewSource(threadClient, SOURCE_URL); + loadSubScript(SOURCE_URL, global); + let { source } = yield promise; + let sourceClient = threadClient.source(source); + + let location = { line: 4 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_false(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", global); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value.type, "undefined"); + do_check_eq(variables.b.value.type, "undefined"); + do_check_eq(variables.c.value.type, "undefined"); + + yield resume(threadClient); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js new file mode 100644 index 000000000..2e841fe19 --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js @@ -0,0 +1,58 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-no-offsets-in-gcd-script.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + let global = createTestGlobal("test"); + loadSubScript(SOURCE_URL, global); + Cu.forceGC(); Cu.forceGC(); Cu.forceGC(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let { sources } = yield getSources(threadClient); + let source = findSource(sources, SOURCE_URL); + let sourceClient = threadClient.source(source); + + let location = { line: 7 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_true(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + reload(tabClient).then(function () { + loadSubScript(SOURCE_URL, global); + }); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, 8); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value, 1); + do_check_eq(variables.c.value.type, "undefined"); + + yield resume(threadClient); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets.js new file mode 100644 index 000000000..5959b23ef --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-line-with-no-offsets.js @@ -0,0 +1,57 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-no-offsets.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + + let global = createTestGlobal("test"); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let promise = waitForNewSource(threadClient, SOURCE_URL); + loadSubScript(SOURCE_URL, global); + let { source } = yield promise; + let sourceClient = threadClient.source(source); + + let location = { line: 5 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_false(packet.isPending); + do_check_true("actualLocation" in packet); + let actualLocation = packet.actualLocation; + do_check_eq(actualLocation.line, 6); + + packet = yield executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", global); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, actualLocation.line); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value, 1); + do_check_eq(variables.c.value.type, "undefined"); + + yield resume(threadClient); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_setBreakpoint-on-line.js b/devtools/server/tests/unit/test_setBreakpoint-on-line.js new file mode 100644 index 000000000..1dab6a633 --- /dev/null +++ b/devtools/server/tests/unit/test_setBreakpoint-on-line.js @@ -0,0 +1,57 @@ +"use strict"; + +var SOURCE_URL = getFileUrl("setBreakpoint-on-line.js"); + +function run_test() { + return Task.spawn(function* () { + do_test_pending(); + + DebuggerServer.registerModule("xpcshell-test/testactors"); + DebuggerServer.init(() => true); + + let global = createTestGlobal("test"); + DebuggerServer.addTestGlobal(global); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let tab = findTab(tabs, "test"); + let [, tabClient] = yield attachTab(client, tab); + + let [, threadClient] = yield attachThread(tabClient); + yield resume(threadClient); + + let promise = waitForNewSource(threadClient, SOURCE_URL); + loadSubScript(SOURCE_URL, global); + let { source } = yield promise; + let sourceClient = threadClient.source(source); + + let location = { line: 5 }; + let [packet, breakpointClient] = yield setBreakpoint(sourceClient, location); + do_check_false(packet.isPending); + do_check_false("actualLocation" in packet); + + packet = yield executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", global); + }, client); + do_check_eq(packet.type, "paused"); + let why = packet.why; + do_check_eq(why.type, "breakpoint"); + do_check_eq(why.actors.length, 1); + do_check_eq(why.actors[0], breakpointClient.actor); + let frame = packet.frame; + let where = frame.where; + do_check_eq(where.source.actor, source.actor); + do_check_eq(where.line, location.line); + let variables = frame.environment.bindings.variables; + do_check_eq(variables.a.value, 1); + do_check_eq(variables.b.value.type, "undefined"); + do_check_eq(variables.c.value.type, "undefined"); + + yield resume(threadClient); + yield close(client); + + do_test_finished(); + }); +} diff --git a/devtools/server/tests/unit/test_source-01.js b/devtools/server/tests/unit/test_source-01.js new file mode 100644 index 000000000..3401cc660 --- /dev/null +++ b/devtools/server/tests/unit/test_source-01.js @@ -0,0 +1,78 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gDebuggee; +var gClient; +var gThreadClient; + +// This test ensures that we can create SourceActors and SourceClients properly, +// and that they can communicate over the protocol to fetch the source text for +// a given script. + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + Cu.evalInSandbox( + "" + function stopMe(arg1) { + debugger; + }, + gDebuggee, + "1.8", + getFileUrl("test_source-01.js") + ); + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_source(); + }); + }); + do_test_pending(); +} + +const SOURCE_URL = "http://example.com/foobar.js"; +const SOURCE_CONTENT = "stopMe()"; + +function test_source() +{ + DebuggerServer.LONG_STRING_LENGTH = 200; + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.getSources(function (aResponse) { + do_check_true(!!aResponse); + do_check_true(!!aResponse.sources); + + let source = aResponse.sources.filter(function (s) { + return s.url === SOURCE_URL; + })[0]; + + do_check_true(!!source); + + let sourceClient = gThreadClient.source(source); + sourceClient.source(function (aResponse) { + do_check_true(!!aResponse); + do_check_true(!aResponse.error); + do_check_true(!!aResponse.contentType); + do_check_true(aResponse.contentType.includes("javascript")); + + do_check_true(!!aResponse.source); + do_check_eq(SOURCE_CONTENT, + aResponse.source); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + + Cu.evalInSandbox( + SOURCE_CONTENT, + gDebuggee, + "1.8", + SOURCE_URL + ); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-01.js b/devtools/server/tests/unit/test_sourcemaps-01.js new file mode 100644 index 000000000..4d92bf9cc --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-01.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic source map integration with the "newSource" packet in the RDP. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_source_map(); + }); + }); + do_test_pending(); +} + +function test_simple_source_map() +{ + // Because we are source mapping, we should be notified of a.js, b.js, and + // c.js as sources, and shouldn't receive abc.js or test_sourcemaps-01.js. + let expectedSources = new Set(["http://example.com/www/js/a.js", + "http://example.com/www/js/b.js", + "http://example.com/www/js/c.js"]); + + gThreadClient.addListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_true(expectedSources.has(aPacket.source.url), + "The source url should be one of our original sources."); + expectedSources.delete(aPacket.source.url); + + if (expectedSources.size === 0) { + gClient.removeListener("newSource", _onNewSource); + finishClient(gClient); + } + }); + + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"), + new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"), + new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-02.js b/devtools/server/tests/unit/test_sourcemaps-02.js new file mode 100644 index 000000000..2a343afaa --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-02.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic source map integration with the "sources" packet in the RDP. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_source_map(); + }); + }); + do_test_pending(); +} + +function test_simple_source_map() +{ + // Because we are source mapping, we should be notified of a.js, b.js, and + // c.js as sources, and shouldn"t receive abc.js or test_sourcemaps-01.js. + let expectedSources = new Set(["http://example.com/www/js/a.js", + "http://example.com/www/js/b.js", + "http://example.com/www/js/c.js"]); + + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.getSources(function (aResponse) { + do_check_true(!aResponse.error, "Should not get an error"); + + for (let s of aResponse.sources) { + do_check_neq(s.url, "http://example.com/www/js/abc.js", + "Shouldn't get the generated source's url."); + expectedSources.delete(s.url); + } + + do_check_eq(expectedSources.size, 0, + "Should have found all the expected sources sources by now."); + + finishClient(gClient); + }); + }); + + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"), + new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"), + new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"), + new SourceNode(1, 0, "d.js", "debugger;\n") + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-03.js b/devtools/server/tests/unit/test_sourcemaps-03.js new file mode 100644 index 000000000..2fbd99aad --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-03.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check setting breakpoints in source mapped sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_source_map(); + }); + }); + do_test_pending(); +} + +function testBreakpointMapping(aName, aCallback) +{ + Task.spawn(function* () { + let response = yield waitForPause(gThreadClient); + do_check_eq(response.why.type, "debuggerStatement"); + + const source = yield getSource(gThreadClient, "http://example.com/www/js/" + aName + ".js"); + response = yield setBreakpoint(source, { + // Setting the breakpoint on an empty line so that it is pushed down one + // line and we can check the source mapped actualLocation later. + line: 3 + }); + + // Should not slide breakpoints for sourcemapped sources + do_check_true(!response.actualLocation); + + yield setBreakpoint(source, { line: 4 }); + + // The eval will cause us to resume, then we get an unsolicited pause + // because of our breakpoint, we resume again to finish the eval, and + // finally receive our last pause which has the result of the client + // evaluation. + response = yield gThreadClient.eval(null, aName + "()"); + do_check_eq(response.type, "resumed"); + + response = yield waitForPause(gThreadClient); + do_check_eq(response.why.type, "breakpoint"); + // Assert that we paused because of the breakpoint at the correct + // location in the code by testing that the value of `ret` is still + // undefined. + do_check_eq(response.frame.environment.bindings.variables.ret.value.type, + "undefined"); + + response = yield resume(gThreadClient); + + response = yield waitForPause(gThreadClient); + do_check_eq(response.why.type, "clientEvaluated"); + do_check_eq(response.why.frameFinished.return, aName); + + response = yield resume(gThreadClient); + + aCallback(); + }); + + gDebuggee.eval("(" + function () { + debugger; + } + "());"); +} + +function test_simple_source_map() +{ + let expectedSources = new Set([ + "http://example.com/www/js/a.js", + "http://example.com/www/js/b.js", + "http://example.com/www/js/c.js" + ]); + + gThreadClient.addListener("newSource", function _onNewSource(aEvent, aPacket) { + expectedSources.delete(aPacket.source.url); + if (expectedSources.size > 0) { + return; + } + gThreadClient.removeListener("newSource", _onNewSource); + + testBreakpointMapping("a", function () { + testBreakpointMapping("b", function () { + testBreakpointMapping("c", function () { + finishClient(gClient); + }); + }); + }); + }); + + let a = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() {\n"), + new SourceNode(2, 0, "a.js", " var ret;\n"), + new SourceNode(3, 0, "a.js", " // Empty line\n"), + new SourceNode(4, 0, "a.js", " ret = 'a';\n"), + new SourceNode(5, 0, "a.js", " return ret;\n"), + new SourceNode(6, 0, "a.js", "}\n") + ]); + let b = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "b.js", "function b() {\n"), + new SourceNode(2, 0, "b.js", " var ret;\n"), + new SourceNode(3, 0, "b.js", " // Empty line\n"), + new SourceNode(4, 0, "b.js", " ret = 'b';\n"), + new SourceNode(5, 0, "b.js", " return ret;\n"), + new SourceNode(6, 0, "b.js", "}\n") + ]); + let c = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "c.js", "function c() {\n"), + new SourceNode(2, 0, "c.js", " var ret;\n"), + new SourceNode(3, 0, "c.js", " // Empty line\n"), + new SourceNode(4, 0, "c.js", " ret = 'c';\n"), + new SourceNode(5, 0, "c.js", " return ret;\n"), + new SourceNode(6, 0, "c.js", "}\n") + ]); + + let { code, map } = (new SourceNode(null, null, null, [ + a, b, c + ])).toStringWithSourceMap({ + file: "http://example.com/www/js/abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-04.js b/devtools/server/tests/unit/test_sourcemaps-04.js new file mode 100644 index 000000000..5fecb44c8 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-04.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that absolute source map urls work. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_absolute_source_map(); + }); + }); + do_test_pending(); +} + +function test_absolute_source_map() +{ + gThreadClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_true(aPacket.source.url.indexOf("sourcemapped.coffee") !== -1, + "The new source should be a coffee file."); + do_check_eq(aPacket.source.url.indexOf("sourcemapped.js"), -1, + "The new source should not be a js file."); + + finishClient(gClient); + }); + + code = readFile("sourcemapped.js") + + "\n//# sourceMappingURL=" + getFileUrl("source-map-data/sourcemapped.map"); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + getFileUrl("sourcemapped.js"), 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-05.js b/devtools/server/tests/unit/test_sourcemaps-05.js new file mode 100644 index 000000000..edc9178db --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-05.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that relative source map urls work. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_relative_source_map(); + }); + }); + do_test_pending(); +} + +function test_relative_source_map() +{ + gThreadClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_true(aPacket.source.url.indexOf("sourcemapped.coffee") !== -1, + "The new source should be a coffee file."); + do_check_eq(aPacket.source.url.indexOf("sourcemapped.js"), -1, + "The new source should not be a js file."); + + finishClient(gClient); + }); + + code = readFile("sourcemapped.js") + + "\n//# sourceMappingURL=source-map-data/sourcemapped.map"; + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + getFileUrl("sourcemapped.js"), 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-06.js b/devtools/server/tests/unit/test_sourcemaps-06.js new file mode 100644 index 000000000..41b518753 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-06.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we can load sources whose content is embedded in the + * "sourcesContent" field of a source map. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_source_content(); + }); + }); + do_test_pending(); +} + +function test_source_content() +{ + let numNewSources = 0; + + gThreadClient.addListener("newSource", function _onNewSource(aEvent, aPacket) { + if (++numNewSources !== 3) { + return; + } + gThreadClient.removeListener("newSource", _onNewSource); + + gThreadClient.getSources(function (aResponse) { + do_check_true(!aResponse.error, "Should not get an error"); + + testContents(aResponse.sources, 0, (timesCalled) => { + do_check_eq(timesCalled, 3); + finishClient(gClient); + }); + }); + }); + + let node = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"), + new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"), + new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"), + ]); + + node.setSourceContent("a.js", "content for a.js"); + node.setSourceContent("b.js", "content for b.js"); + node.setSourceContent("c.js", "content for c.js"); + + let { code, map } = node.toStringWithSourceMap({ + file: "abc.js" + }); + + code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} + +function testContents(sources, timesCalled, callback) { + if (sources.length === 0) { + callback(timesCalled); + return; + } + + + let source = sources[0]; + let sourceClient = gThreadClient.source(sources[0]); + + if (sourceClient.url) { + sourceClient.source((aResponse) => { + do_check_true(!aResponse.error, + "Should not get an error loading the source from sourcesContent"); + + let expectedContent = "content for " + source.url; + do_check_eq(aResponse.source, expectedContent, + "Should have the expected source content"); + + testContents(sources.slice(1), timesCalled + 1, callback); + }); + } + else { + testContents(sources.slice(1), timesCalled, callback); + } +} diff --git a/devtools/server/tests/unit/test_sourcemaps-07.js b/devtools/server/tests/unit/test_sourcemaps-07.js new file mode 100644 index 000000000..b8a9462c0 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-07.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we don't permanently cache sources from source maps. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_cached_original_sources(); + }); + }); + do_test_pending(); +} + +function test_cached_original_sources() +{ + writeFile("temp.js", "initial content"); + + gThreadClient.addOneTimeListener("newSource", onNewSource); + + let node = new SourceNode(1, 0, + getFileUrl("temp.js"), + "function funcFromTemp() {}\n"); + let { code, map } = node.toStringWithSourceMap({ + file: "abc.js" + }); + code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} + +function onNewSource(aEvent, aPacket) { + let sourceClient = gThreadClient.source(aPacket.source); + sourceClient.source(function (aResponse) { + do_check_true(!aResponse.error, + "Should not be an error grabbing the source"); + do_check_eq(aResponse.source, "initial content", + "The correct source content should be sent"); + + writeFile("temp.js", "new content"); + + sourceClient.source(function (aResponse) { + do_check_true(!aResponse.error, + "Should not be an error grabbing the source"); + do_check_eq(aResponse.source, "new content", + "The correct source content should not be cached, so we should get the new content"); + + do_get_file("temp.js").remove(false); + finishClient(gClient); + }); + }); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-08.js b/devtools/server/tests/unit/test_sourcemaps-08.js new file mode 100644 index 000000000..b23665e43 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-08.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Regression test for bug 882986 regarding sourcesContent and absolute source + * URLs. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_source_maps(); + }); + }); + do_test_pending(); +} + +function test_source_maps() +{ + gThreadClient.addOneTimeListener("newSource", function (aEvent, aPacket) { + let sourceClient = gThreadClient.source(aPacket.source); + sourceClient.source(function ({error, source}) { + do_check_true(!error, "should be able to grab the source"); + do_check_eq(source, "foo", + "Should load the source from the sourcesContent field"); + finishClient(gClient); + }); + }); + + let code = "'nothing here';\n"; + code += "//# sourceMappingURL=data:text/json," + JSON.stringify({ + version: 3, + file: "foo.js", + sources: ["/a"], + names: [], + mappings: "AACA", + sourcesContent: ["foo"] + }); + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/foo.js", 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-09.js b/devtools/server/tests/unit/test_sourcemaps-09.js new file mode 100644 index 000000000..c317cf723 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-09.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that source maps and breakpoints work with minified javascript. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_minified(); + }); + }); + do_test_pending(); +} + +function test_minified() +{ + let newSourceFired = false; + + gThreadClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_eq(aPacket.source.url, "http://example.com/foo.js", + "The new source should be foo.js"); + do_check_eq(aPacket.source.url.indexOf("foo.min.js"), -1, + "The new source should not be the minified file"); + + newSourceFired = true; + }); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aEvent, "paused"); + do_check_eq(aPacket.why.type, "debuggerStatement"); + + let location = { + line: 5 + }; + + getSource(gThreadClient, "http://example.com/foo.js").then(source => { + source.setBreakpoint(location, function (aResponse, bpClient) { + do_check_true(!aResponse.error); + testHitBreakpoint(); + }); + }); + }); + + // This is the original foo.js, which was then minified with uglifyjs version + // 2.2.5 and the "--mangle" option. + // + // (function () { + // debugger; + // function foo(n) { + // var bar = n + n; + // var unused = null; + // return bar; + // } + // for (var i = 0; i < 10; i++) { + // foo(i); + // } + // }()); + + let code = '(function(){debugger;function r(r){var n=r+r;var u=null;return n}for(var n=0;n<10;n++){r(n)}})();\n//# sourceMappingURL=data:text/json,{"file":"foo.min.js","version":3,"sources":["foo.js"],"names":["foo","n","bar","unused","i"],"mappings":"CAAC,WACC,QACA,SAASA,GAAIC,GACX,GAAIC,GAAMD,EAAIA,CACd,IAAIE,GAAS,IACb,OAAOD,GAET,IAAK,GAAIE,GAAI,EAAGA,EAAI,GAAIA,IAAK,CAC3BJ,EAAII"}'; + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/foo.min.js", 1); +} + +function testHitBreakpoint(timesHit = 0) { + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + ++timesHit; + + do_check_eq(aEvent, "paused"); + do_check_eq(aPacket.why.type, "breakpoint"); + + if (timesHit === 10) { + gThreadClient.resume(() => finishClient(gClient)); + } else { + testHitBreakpoint(timesHit); + } + }); + + gThreadClient.resume(); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-10.js b/devtools/server/tests/unit/test_sourcemaps-10.js new file mode 100644 index 000000000..e955dc8fb --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-10.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we source map frame locations for the frame we are paused at. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() { + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + promise.resolve(define_code()) + .then(run_code) + .then(test_frame_location) + .then(null, error => { + dump(error + "\n"); + dump(error.stack); + do_check_true(false); + }) + .then(() => { + finishClient(gClient); + }); + }); + }); + do_test_pending(); +} + +function define_code() { + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() {\n"), + new SourceNode(2, 0, "a.js", " b();\n"), + new SourceNode(3, 0, "a.js", "}\n"), + new SourceNode(1, 0, "b.js", "function b() {\n"), + new SourceNode(2, 0, "b.js", " c();\n"), + new SourceNode(3, 0, "b.js", "}\n"), + new SourceNode(1, 0, "c.js", "function c() {\n"), + new SourceNode(2, 0, "c.js", " debugger;\n"), + new SourceNode(3, 0, "c.js", "}\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//# sourceMappingURL=data:text/json," + map.toString(); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} + +function run_code() { + const d = promise.defer(); + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + d.resolve(aPacket); + gThreadClient.resume(); + }); + gDebuggee.a(); + return d.promise; +} + +function test_frame_location({ frame: { where: { source, line, column } } }) { + do_check_eq(source.url, "http://example.com/www/js/c.js"); + do_check_eq(line, 2); + do_check_eq(column, 0); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-11.js b/devtools/server/tests/unit/test_sourcemaps-11.js new file mode 100644 index 000000000..e598c4c09 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-11.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we source map frame locations returned by "frames" requests. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() { + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + promise.resolve(define_code()) + .then(run_code) + .then(test_frames) + .then(null, error => { + dump(error + "\n"); + dump(error.stack); + do_check_true(false); + }) + .then(() => { + finishClient(gClient); + }); + }); + }); + do_test_pending(); +} + +function define_code() { + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() {\n"), + new SourceNode(2, 0, "a.js", " b();\n"), + new SourceNode(3, 0, "a.js", "}\n"), + new SourceNode(1, 0, "b.js", "function b() {\n"), + new SourceNode(2, 0, "b.js", " c();\n"), + new SourceNode(3, 0, "b.js", "}\n"), + new SourceNode(1, 0, "c.js", "function c() {\n"), + new SourceNode(2, 0, "c.js", " debugger;\n"), + new SourceNode(3, 0, "c.js", "}\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//# sourceMappingURL=data:text/json," + map.toString(); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} + +function run_code() { + const d = promise.defer(); + gClient.addOneTimeListener("paused", function () { + gThreadClient.getFrames(0, 3, function (aResponse) { + d.resolve(aResponse); + gThreadClient.resume(); + }); + }); + gDebuggee.a(); + return d.promise; +} + +function test_frames({ error, frames }) { + do_check_true(!error); + do_check_eq(frames.length, 3); + check_frame(frames[0], "http://example.com/www/js/c.js"); + check_frame(frames[1], "http://example.com/www/js/b.js"); + check_frame(frames[2], "http://example.com/www/js/a.js"); +} + +function check_frame({ where: { source, line, column } }, aExpectedUrl) { + do_check_eq(source.url, aExpectedUrl); + do_check_eq(line, 2); + do_check_eq(column, 0); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-12.js b/devtools/server/tests/unit/test_sourcemaps-12.js new file mode 100644 index 000000000..cb7f29920 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-12.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we continue stepping when a single original source's line + * corresponds to multiple generated js lines. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() { + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + define_code(); + }); + }); + do_test_pending(); +} + +function define_code() { + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function runTest() {\n"), + // A bunch of js lines map to the same original source line. + new SourceNode(2, 0, "a.js", " debugger;\n"), + new SourceNode(2, 0, "a.js", " var sum = 0;\n"), + new SourceNode(2, 0, "a.js", " for (var i = 0; i < 5; i++) {\n"), + new SourceNode(2, 0, "a.js", " sum += i;\n"), + new SourceNode(2, 0, "a.js", " }\n"), + // And now we have a new line in the original source that we should stop at. + new SourceNode(3, 0, "a.js", " sum;\n"), + new SourceNode(3, 0, "a.js", "}\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/" + }); + + code += "//# sourceMappingURL=data:text/json," + map.toString(); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/abc.js", 1); + + run_code(); +} + +function run_code() { + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "debuggerStatement"); + step_in(); + }); + gDebuggee.runTest(); +} + +function step_in() { + gClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "resumeLimit"); + let { frame: { environment, where: { source, line } } } = aPacket; + // Stepping should have moved us to the next source mapped line. + do_check_eq(source.url, "http://example.com/a.js"); + do_check_eq(line, 3); + // Which should have skipped over the for loop in the generated js and sum + // should be calculated. + do_check_eq(environment.bindings.variables.sum.value, 10); + finishClient(gClient); + }); + gThreadClient.stepIn(); +} + diff --git a/devtools/server/tests/unit/test_sourcemaps-13.js b/devtools/server/tests/unit/test_sourcemaps-13.js new file mode 100644 index 000000000..203731fda --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-13.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we don't permanently cache source maps across reloads. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gTabClient; + +const {SourceNode} = require("source-map"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + gTabClient = aTabClient; + setup_code(); + }); + }); + do_test_pending(); +} + +// The MAP_FILE_NAME is .txt so that the OS will definitely have an extension -> +// content type mapping for the extension. If it doesn't (like .map or .json), +// it logs console errors, which cause the test to fail. See bug 907839. +const MAP_FILE_NAME = "temporary-generated.txt"; + +const TEMP_FILE_1 = "temporary1.js"; +const TEMP_FILE_2 = "temporary2.js"; +const TEMP_GENERATED_SOURCE = "temporary-generated.js"; + +function setup_code() { + let node = new SourceNode(1, 0, + getFileUrl(TEMP_FILE_1, true), + "function temporary1() {}\n"); + let { code, map } = node.toStringWithSourceMap({ + file: getFileUrl(TEMP_GENERATED_SOURCE, true) + }); + + code += "//# sourceMappingURL=" + getFileUrl(MAP_FILE_NAME, true); + writeFile(MAP_FILE_NAME, map.toString()); + + Cu.evalInSandbox(code, + gDebuggee, + "1.8", + getFileUrl(TEMP_GENERATED_SOURCE, true), + 1); + + test_initial_sources(); +} + +function test_initial_sources() { + gThreadClient.getSources(function ({ error, sources }) { + do_check_true(!error); + sources = sources.filter(source => source.url); + do_check_eq(sources.length, 1); + do_check_eq(sources[0].url, getFileUrl(TEMP_FILE_1, true)); + reload(gTabClient).then(setup_new_code); + }); +} + +function setup_new_code() { + let node = new SourceNode(1, 0, + getFileUrl(TEMP_FILE_2, true), + "function temporary2() {}\n"); + let { code, map } = node.toStringWithSourceMap({ + file: getFileUrl(TEMP_GENERATED_SOURCE, true) + }); + + code += "\n//# sourceMappingURL=" + getFileUrl(MAP_FILE_NAME, true); + writeFile(MAP_FILE_NAME, map.toString()); + + gThreadClient.addOneTimeListener("newSource", test_new_sources); + Cu.evalInSandbox(code, + gDebuggee, + "1.8", + getFileUrl(TEMP_GENERATED_SOURCE, true), + 1); +} + +function test_new_sources() { + gThreadClient.getSources(function ({ error, sources }) { + do_check_true(!error); + sources = sources.filter(source => source.url); + + // Should now have TEMP_FILE_2 as a source. + do_check_eq(sources.length, 1); + let s = sources.filter(s => s.url === getFileUrl(TEMP_FILE_2, true))[0]; + do_check_true(!!s); + + finish_test(); + }); +} + +function finish_test() { + do_get_file(MAP_FILE_NAME).remove(false); + finishClient(gClient); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-16.js b/devtools/server/tests/unit/test_sourcemaps-16.js new file mode 100644 index 000000000..4df9ece23 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-16.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that we can load the contents of every source in a source map produced + * by babel and browserify. + */ + +var gDebuggee; +var gClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-sourcemaps"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestThread(gClient, "test-sourcemaps", testSourcemap); + }); + do_test_pending(); +} + +const testSourcemap = Task.async(function* (threadResponse, tabClient, threadClient, tabResponse) { + evalTestCode(); + + const { sources } = yield getSources(threadClient); + + for (let form of sources) { + let sourceResponse = yield getSourceContent(threadClient.source(form)); + ok(sourceResponse, "Should be able to get the source response"); + ok(sourceResponse.source, "Should have the source text as well"); + } + + finishClient(gClient); +}); + +const TEST_FILE = "babel_and_browserify_script_with_source_map.js"; + +function evalTestCode() { + const testFileContents = readFile(TEST_FILE); + Cu.evalInSandbox(testFileContents, + gDebuggee, + "1.8", + getFileUrl(TEST_FILE), + 1); +} diff --git a/devtools/server/tests/unit/test_sourcemaps-17.js b/devtools/server/tests/unit/test_sourcemaps-17.js new file mode 100644 index 000000000..85da95972 --- /dev/null +++ b/devtools/server/tests/unit/test_sourcemaps-17.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that we properly handle frames that cannot be sourcemapped + * when using sourcemaps. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +const {SourceNode} = require("source-map"); + +function run_test() { + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-source-map", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_source_map(); + }); + }); + do_test_pending(); +} + +function test_source_map() { + // Set up debuggee code. + const a = new SourceNode(1, 1, "a.js", "function a() { b(); }"); + const b = new SourceNode(null, null, null, "function b() { c(); }"); + const c = new SourceNode(1, 1, "c.js", "function c() { d(); }"); + const d = new SourceNode(null, null, null, "function d() { e(); }"); + const e = new SourceNode(1, 1, "e.js", "function e() { debugger; }"); + const { map, code } = (new SourceNode(null, null, null, [a, b, c, d, e])).toStringWithSourceMap({ + file: "root.js", + sourceRoot: "root", + }); + Components.utils.evalInSandbox( + code + "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()), + gDebuggee, + "1.8", + "http://example.com/www/js/abc.js", + 1 + ); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.getFrames(0, 50, function ({ error, frames }) { + do_check_true(!error); + do_check_eq(frames.length, 4); + // b.js should be skipped + do_check_eq(frames[0].where.source.url, "http://example.com/www/root/e.js"); + do_check_eq(frames[1].where.source.url, "http://example.com/www/root/c.js"); + do_check_eq(frames[2].where.source.url, "http://example.com/www/root/a.js"); + do_check_eq(frames[3].where.source.url, null); + + finishClient(gClient); + }); + }); + + // Trigger it. + gDebuggee.eval("a()"); +} diff --git a/devtools/server/tests/unit/test_stepping-01.js b/devtools/server/tests/unit/test_stepping-01.js new file mode 100644 index 000000000..593a485a1 --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-01.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic step-over functionality. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_stepping(); + }); + }); +} + +function test_simple_stepping() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 2); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, undefined); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + // When leaving a stack frame the line number doesn't change. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, 2); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + gThreadClient.stepOver(); + }); + gThreadClient.stepOver(); + + }); + gThreadClient.stepOver(); + + }); + + gDebuggee.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n"); // line0 + 3 +} diff --git a/devtools/server/tests/unit/test_stepping-02.js b/devtools/server/tests/unit/test_stepping-02.js new file mode 100644 index 000000000..eaee099b9 --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-02.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic step-in functionality. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_stepping(); + }); + }); +} + +function test_simple_stepping() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 2); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, undefined); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + // When leaving a stack frame the line number doesn't change. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, 2); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + gThreadClient.stepIn(); + }); + gThreadClient.stepIn(); + + }); + gThreadClient.stepIn(); + + }); + + gDebuggee.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n"); // line0 + 3 +} diff --git a/devtools/server/tests/unit/test_stepping-03.js b/devtools/server/tests/unit/test_stepping-03.js new file mode 100644 index 000000000..6123928d7 --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-03.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic step-out functionality. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_stepping(); + }); + }); +} + +function test_simple_stepping() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, 2); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + gThreadClient.stepOut(); + + }); + + gDebuggee.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " debugger;\n" + // line0 + 2 + " this.a = 1;\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "f();\n"); // line0 + 6 +} diff --git a/devtools/server/tests/unit/test_stepping-04.js b/devtools/server/tests/unit/test_stepping-04.js new file mode 100644 index 000000000..efe52200d --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-04.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that stepping over a function call does not pause inside the function. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_stepping(); + }); + }); +} + +function test_simple_stepping() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, undefined); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + gThreadClient.stepOver(); + + }); + gThreadClient.stepOver(); + + }); + + gDebuggee.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + "}\n" + // line0 + 3 + "debugger;\n" + // line0 + 4 + "f();\n" + // line0 + 5 + "let b = 2;\n"); // line0 + 6 +} diff --git a/devtools/server/tests/unit/test_stepping-05.js b/devtools/server/tests/unit/test_stepping-05.js new file mode 100644 index 000000000..2ab4570af --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-05.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that stepping in the last statement of the last frame doesn't + * cause an unexpected pause, when another JS frame is pushed on the stack + * (bug 785689). + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_stepping_last(); + }); + }); +} + +function test_stepping_last() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 2); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, undefined); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + // When leaving a stack frame the line number doesn't change. + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3); + do_check_eq(aPacket.why.type, "resumeLimit"); + // Check that stepping worked. + do_check_eq(gDebuggee.a, 1); + do_check_eq(gDebuggee.b, 2); + + gThreadClient.stepIn(function () { + test_next_pause(); + }); + }); + gThreadClient.stepIn(); + }); + gThreadClient.stepIn(); + + }); + gThreadClient.stepIn(); + + }); + + gDebuggee.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n"); // line0 + 3 +} + +function test_next_pause() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check the return value. + do_check_eq(aPacket.type, "paused"); + // Before fixing bug 785689, the type was resumeLimit. + do_check_eq(aPacket.why.type, "debuggerStatement"); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + + gDebuggee.eval("debugger;"); +} diff --git a/devtools/server/tests/unit/test_stepping-06.js b/devtools/server/tests/unit/test_stepping-06.js new file mode 100644 index 000000000..49689f830 --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-06.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that stepping out of a function returns the right return value. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gCallback; + +function run_test() +{ + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); + do_test_pending(); +} + +function run_test_with_server(aServer, aCallback) +{ + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stack", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + // XXX: We have to do an executeSoon so that the error isn't caught and + // reported by DebuggerClient.requester (because we are using the local + // transport and share a stack) which causes the test to fail. + Services.tm.mainThread.dispatch({ + run: test_simple_stepping + }, Ci.nsIThread.DISPATCH_NORMAL); + }); + }); +} + +function test_simple_stepping() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check that the return value is 10. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.why.frameFinished.return, 10); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check that the return value is undefined. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 8); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.why.frameFinished.return.type, "undefined"); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Check that the exception was thrown. + do_check_eq(aPacket.type, "paused"); + do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 11); + do_check_eq(aPacket.why.type, "resumeLimit"); + do_check_eq(aPacket.why.frameFinished.throw, "ah"); + + gThreadClient.resume(function () { + gClient.close().then(gCallback); + }); + }); + gThreadClient.stepOut(); + }); + gThreadClient.resume(); + }); + gThreadClient.stepOut(); + }); + gThreadClient.resume(); + }); + gThreadClient.stepOut(); + + }); + + gDebuggee.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " debugger;\n" + // line0 + 2 + " var a = 10;\n" + // line0 + 3 + " return a;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "function g() {\n" + // line0 + 6 + " debugger;\n" + // line0 + 7 + "}\n" + // line0 + 8 + "function h() {\n" + // line0 + 9 + " debugger;\n" + // line0 + 10 + " throw 'ah';\n" + // line0 + 11 + " return 2;\n" + // line0 + 12 + "}\n" + // line0 + 13 + "f();\n" + // line0 + 14 + "g();\n" + // line0 + 15 + "try { h() } catch (ex) { };\n"); // line0 + 16 +} diff --git a/devtools/server/tests/unit/test_stepping-07.js b/devtools/server/tests/unit/test_stepping-07.js new file mode 100644 index 000000000..9ad0d79ff --- /dev/null +++ b/devtools/server/tests/unit/test_stepping-07.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that stepping over an implicit return makes sense. Bug 1155966. + */ + +var gDebuggee; +var gClient; +var gCallback; + +function run_test() { + do_test_pending(); + run_test_with_server(DebuggerServer, function () { + run_test_with_server(WorkerDebuggerServer, do_test_finished); + }); +} + +function run_test_with_server(aServer, aCallback) { + gCallback = aCallback; + initTestDebuggerServer(aServer); + gDebuggee = addTestGlobal("test-stepping", aServer); + gClient = new DebuggerClient(aServer.connectPipe()); + gClient.connect(testSteppingAndReturns); +} + +const testSteppingAndReturns = Task.async(function* () { + const [attachResponse, tabClient, threadClient] = yield attachTestTabAndResume(gClient, "test-stepping"); + ok(!attachResponse.error, "Should not get an error attaching"); + + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt1 = yield executeOnNextTickAndWaitForPause(evaluateTestCode, gClient); + equal(dbgStmt1.frame.where.line, 3, + "Should be at debugger statement on line 3"); + + dumpn("Testing stepping with implicit return"); + const step1 = yield stepOver(gClient, threadClient); + equal(step1.frame.where.line, 4, "Should step to line 4"); + const step2 = yield stepOver(gClient, threadClient); + equal(step2.frame.where.line, 7, + "Should step to line 7, the implicit return at the last line of the function"); + // This assertion doesn't pass yet. You would need to do *another* + // step at the end of this function to get the frameFinished. + // See bug 923975. + // + // ok(step2.why.frameFinished, "This should be the implicit function return"); + + dumpn("Continuing and waiting for second debugger statement"); + const dbgStmt2 = yield resumeAndWaitForPause(gClient, threadClient); + equal(dbgStmt2.frame.where.line, 12, + "Should be at debugger statement on line 3"); + + dumpn("Testing stepping with explicit return"); + const step3 = yield stepOver(gClient, threadClient); + equal(step3.frame.where.line, 13, "Should step to line 13"); + const step4 = yield stepOver(gClient, threadClient); + equal(step4.frame.where.line, 15, "Should step out of the function from line 15"); + // This step is a bit funny, see bug 1013219 for details. + const step5 = yield stepOver(gClient, threadClient); + equal(step5.frame.where.line, 15, "Should step out of the function from line 15"); + ok(step5.why.frameFinished, "This should be the explicit function return"); + + finishClient(gClient, gCallback); +}); + +function evaluateTestCode() { + Cu.evalInSandbox( + ` // 1 + function implicitReturn() { // 2 + debugger; // 3 + if (this.someUndefinedProperty) { // 4 + yikes(); // 5 + } // 6 + } // 7 + // 8 + var yes = true; // 9 + function explicitReturn() { // 10 + if (yes) { // 11 + debugger; // 12 + return 1; // 13 + } // 14 + } // 15 + // 16 + implicitReturn(); // 17 + explicitReturn(); // 18 + `, // 19 + gDebuggee, + "1.8", + "test_stepping-07-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/unit/test_symbols-01.js b/devtools/server/tests/unit/test_symbols-01.js new file mode 100644 index 000000000..8ea26086a --- /dev/null +++ b/devtools/server/tests/unit/test_symbols-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can represent ES6 Symbols over the RDP. + */ + +const URL = "foo.js"; + +function run_test() { + initTestDebuggerServer(); + const debuggee = addTestGlobal("test-symbols"); + const client = new DebuggerClient(DebuggerServer.connectPipe()); + + client.connect().then(function () { + attachTestTabAndResume(client, "test-symbols", function (response, tabClient, threadClient) { + add_task(testSymbols.bind(null, client, debuggee)); + run_next_test(); + }); + }); + + do_test_pending(); +} + +function* testSymbols(client, debuggee) { + const evalCode = () => { + Components.utils.evalInSandbox( + "(" + function () { + var symbolWithName = Symbol("Chris"); + var symbolWithoutName = Symbol(); + var iteratorSymbol = Symbol.iterator; + debugger; + } + "())", + debuggee, + "1.8", + URL, + 1 + ); + }; + + const packet = yield executeOnNextTickAndWaitForPause(evalCode, client); + const { + symbolWithName, + symbolWithoutName, + iteratorSymbol + } = packet.frame.environment.bindings.variables; + + equal(symbolWithName.value.type, "symbol"); + equal(symbolWithName.value.name, "Chris"); + + equal(symbolWithoutName.value.type, "symbol"); + ok(!("name" in symbolWithoutName.value)); + + equal(iteratorSymbol.value.type, "symbol"); + equal(iteratorSymbol.value.name, "Symbol.iterator"); + + finishClient(client); +} diff --git a/devtools/server/tests/unit/test_symbols-02.js b/devtools/server/tests/unit/test_symbols-02.js new file mode 100644 index 000000000..f80dc3322 --- /dev/null +++ b/devtools/server/tests/unit/test_symbols-02.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we don't run debuggee code when getting symbol names. + */ + +const URL = "foo.js"; + +function run_test() { + initTestDebuggerServer(); + const debuggee = addTestGlobal("test-symbols"); + const client = new DebuggerClient(DebuggerServer.connectPipe()); + + client.connect().then(function () { + attachTestTabAndResume(client, "test-symbols", function (response, tabClient, threadClient) { + add_task(testSymbols.bind(null, client, debuggee)); + run_next_test(); + }); + }); + + do_test_pending(); +} + +function* testSymbols(client, debuggee) { + const evalCode = () => { + Components.utils.evalInSandbox( + "(" + function () { + Symbol.prototype.toString = () => { + throw new Error("lololol"); + }; + var sym = Symbol("le troll"); + debugger; + } + "())", + debuggee, + "1.8", + URL, + 1 + ); + }; + + const packet = yield executeOnNextTickAndWaitForPause(evalCode, client); + const { sym } = packet.frame.environment.bindings.variables; + + equal(sym.value.type, "symbol"); + equal(sym.value.name, "le troll"); + + finishClient(client); +} diff --git a/devtools/server/tests/unit/test_threadlifetime-01.js b/devtools/server/tests/unit/test_threadlifetime-01.js new file mode 100644 index 000000000..1325a45fa --- /dev/null +++ b/devtools/server/tests/unit/test_threadlifetime-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that thread-lifetime grips last past a resume. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_thread_lifetime(); + }); + }); + do_test_pending(); +} + +function test_thread_lifetime() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let pauseGrip = aPacket.frame.arguments[0]; + + // Create a thread-lifetime actor for this object. + gClient.request({ to: pauseGrip.actor, type: "threadGrip" }, function (aResponse) { + // Successful promotion won't return an error. + do_check_eq(aResponse.error, undefined); + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Verify that the promoted actor is returned again. + do_check_eq(pauseGrip.actor, aPacket.frame.arguments[0].actor); + // Now that we've resumed, should get unrecognizePacketType for the + // promoted grip. + gClient.request({ to: pauseGrip.actor, type: "bogusRequest"}, function (aResponse) { + do_check_eq(aResponse.error, "unrecognizedPacketType"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + gThreadClient.resume(); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1) { + debugger; + debugger; + } + stopMe({obj: true}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_threadlifetime-02.js b/devtools/server/tests/unit/test_threadlifetime-02.js new file mode 100644 index 000000000..a7d21a7f9 --- /dev/null +++ b/devtools/server/tests/unit/test_threadlifetime-02.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that thread-lifetime grips last past a resume. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_thread_lifetime(); + }); + }); + do_test_pending(); +} + +function test_thread_lifetime() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let pauseGrip = aPacket.frame.arguments[0]; + + // Create a thread-lifetime actor for this object. + gClient.request({ to: pauseGrip.actor, type: "threadGrip" }, function (aResponse) { + // Successful promotion won't return an error. + do_check_eq(aResponse.error, undefined); + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Verify that the promoted actor is returned again. + do_check_eq(pauseGrip.actor, aPacket.frame.arguments[0].actor); + // Now that we've resumed, release the thread-lifetime grip. + gClient.release(pauseGrip.actor, function (aResponse) { + gClient.request({ to: pauseGrip.actor, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + gThreadClient.resume(function (aResponse) { + finishClient(gClient); + }); + }); + }); + }); + gThreadClient.resume(); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1) { + debugger; + debugger; + } + stopMe({obj: true}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_threadlifetime-03.js b/devtools/server/tests/unit/test_threadlifetime-03.js new file mode 100644 index 000000000..22b707ff8 --- /dev/null +++ b/devtools/server/tests/unit/test_threadlifetime-03.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that thread-lifetime grips last past a resume. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_thread_lifetime(); + }); + }); + do_test_pending(); +} + +function test_thread_lifetime() +{ + // Get three thread-lifetime grips. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let grips = []; + + let handler = function (aResponse) { + if (aResponse.error) { + do_check_eq(aResponse.error, ""); + finishClient(gClient); + } + grips.push(aResponse.from); + if (grips.length == 3) { + test_release_many(grips); + } + }; + for (let i = 0; i < 3; i++) { + gClient.request({ to: aPacket.frame.arguments[i].actor, type: "threadGrip" }, + handler); + } + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1, arg2, arg3) { + debugger; + } + stopMe({obj: 1}, {obj: 2}, {obj: 3}); + } + ")()"); +} + +function test_release_many(grips) +{ + // Release the first two grips, leave the third alive. + + let release = [grips[0], grips[1]]; + + gThreadClient.releaseMany(release, function (aResponse) { + // First two actors should return a noSuchActor error, because + // they're gone now. + gClient.request({ to: grips[0], type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + gClient.request({ to: grips[1], type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + + // Last actor should return unrecognizedPacketType, because it's still + // alive. + gClient.request({ to: grips[2], type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "unrecognizedPacketType"); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + }); +} diff --git a/devtools/server/tests/unit/test_threadlifetime-04.js b/devtools/server/tests/unit/test_threadlifetime-04.js new file mode 100644 index 000000000..aff8b525c --- /dev/null +++ b/devtools/server/tests/unit/test_threadlifetime-04.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that requesting a thread-lifetime actor twice for the same + * value returns the same actor. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_thread_lifetime(); + }); + }); + do_test_pending(); +} + +function test_thread_lifetime() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let pauseGrip = aPacket.frame.arguments[0]; + + gClient.request({ to: pauseGrip.actor, type: "threadGrip" }, function (aResponse) { + // Successful promotion won't return an error. + do_check_eq(aResponse.error, undefined); + + let threadGrip1 = aResponse.from; + + gClient.request({ to: pauseGrip.actor, type: "threadGrip" }, function (aResponse) { + do_check_eq(threadGrip1, aResponse.from); + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1) { + debugger; + } + stopMe({obj: true}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_threadlifetime-05.js b/devtools/server/tests/unit/test_threadlifetime-05.js new file mode 100644 index 000000000..6697947c1 --- /dev/null +++ b/devtools/server/tests/unit/test_threadlifetime-05.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure that releasing a pause-lifetime actorin a releaseMany returns an + * error, but releases all the thread-lifetime actors. + */ + +var gDebuggee; +var gClient; +var gThreadClient; +var gPauseGrip; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_thread_lifetime(); + }); + }); + do_test_pending(); +} + +function arg_grips(aFrameArgs, aOnResponse) { + let grips = []; + let handler = function (aResponse) { + if (aResponse.error) { + grips.push(aResponse.error); + } else { + grips.push(aResponse.from); + } + if (grips.length == aFrameArgs.length) { + aOnResponse(grips); + } + }; + for (let i = 0; i < aFrameArgs.length; i++) { + gClient.request({ to: aFrameArgs[i].actor, type: "threadGrip" }, + handler); + } +} + +function test_thread_lifetime() +{ + // Get two thread-lifetime grips. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + + let frameArgs = [ aPacket.frame.arguments[0], aPacket.frame.arguments[1] ]; + gPauseGrip = aPacket.frame.arguments[2]; + arg_grips(frameArgs, function (aGrips) { + release_grips(frameArgs, aGrips); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1, arg2, arg3) { + debugger; + } + stopMe({obj: 1}, {obj: 2}, {obj: 3}); + } + ")()"); +} + + +function release_grips(aFrameArgs, aThreadGrips) +{ + // Release all actors with releaseMany... + let release = [aThreadGrips[0], aThreadGrips[1], gPauseGrip.actor]; + gThreadClient.releaseMany(release, function (aResponse) { + do_check_eq(aResponse.error, "notReleasable"); + // Now ask for thread grips again, they should not exist. + arg_grips(aFrameArgs, function (aNewGrips) { + for (let i = 0; i < aNewGrips.length; i++) { + do_check_eq(aNewGrips[i], "noSuchActor"); + } + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); +} diff --git a/devtools/server/tests/unit/test_threadlifetime-06.js b/devtools/server/tests/unit/test_threadlifetime-06.js new file mode 100644 index 000000000..dba0156f9 --- /dev/null +++ b/devtools/server/tests/unit/test_threadlifetime-06.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that promoting multiple pause-lifetime actors to thread-lifetime actors + * works as expected. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-grips"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect().then(function () { + attachTestTabAndResume(gClient, "test-grips", function (aResponse, aTabClient, aThreadClient) { + gThreadClient = aThreadClient; + test_thread_lifetime(); + }); + }); + do_test_pending(); +} + +function test_thread_lifetime() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + let actors = []; + let last; + for (let aGrip of aPacket.frame.arguments) { + actors.push(aGrip.actor); + last = aGrip.actor; + } + + // Create thread-lifetime actors for these objects. + gThreadClient.threadGrips(actors, function (aResponse) { + // Successful promotion won't return an error. + do_check_eq(aResponse.error, undefined); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + // Verify that the promoted actors are returned again. + actors.forEach(function (actor, i) { + do_check_eq(actor, aPacket.frame.arguments[i].actor); + }); + // Now that we've resumed, release the thread-lifetime grips. + gThreadClient.releaseMany(actors, function (aResponse) { + // Successful release won't return an error. + do_check_eq(aResponse.error, undefined); + + gClient.request({ to: last, type: "bogusRequest" }, function (aResponse) { + do_check_eq(aResponse.error, "noSuchActor"); + gThreadClient.resume(function (aResponse) { + finishClient(gClient); + }); + }); + }); + }); + gThreadClient.resume(); + }); + }); + + gDebuggee.eval("(" + function () { + function stopMe(arg1, arg2, arg3) { + debugger; + debugger; + } + stopMe({obj: 1}, {obj: 2}, {obj: 3}); + } + ")()"); +} diff --git a/devtools/server/tests/unit/test_unsafeDereference.js b/devtools/server/tests/unit/test_unsafeDereference.js new file mode 100644 index 000000000..d7afe645f --- /dev/null +++ b/devtools/server/tests/unit/test_unsafeDereference.js @@ -0,0 +1,134 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Test Debugger.Object.prototype.unsafeDereference in the presence of +// interesting cross-compartment wrappers. +// +// This is not really a debugger server test; it's more of a Debugger test. +// But we need xpcshell and Components.utils.Sandbox to get +// cross-compartment wrappers with interesting properties, and this is the +// xpcshell test directory most closely related to the JS Debugger API. + +Components.utils.import("resource://gre/modules/jsdebugger.jsm"); +addDebuggerToGlobal(this); + +// Add a method to Debugger.Object for fetching value properties +// conveniently. +Debugger.Object.prototype.getProperty = function (aName) { + let desc = this.getOwnPropertyDescriptor(aName); + if (!desc) + return undefined; + if (!desc.value) { + throw Error("Debugger.Object.prototype.getProperty: " + + "not a value property: " + aName); + } + return desc.value; +}; + +function run_test() { + // Create a low-privilege sandbox, and a chrome-privilege sandbox. + let contentBox = Components.utils.Sandbox("http://www.example.com"); + let chromeBox = Components.utils.Sandbox(this); + + // Create an objects in this compartment, and one in each sandbox. We'll + // refer to the objects as "mainObj", "contentObj", and "chromeObj", in + // variable and property names. + var mainObj = { name: "mainObj" }; + Components.utils.evalInSandbox('var contentObj = { name: "contentObj" };', + contentBox); + Components.utils.evalInSandbox('var chromeObj = { name: "chromeObj" };', + chromeBox); + + // Give each global a pointer to all the other globals' objects. + contentBox.mainObj = chromeBox.mainObj = mainObj; + var contentObj = chromeBox.contentObj = contentBox.contentObj; + var chromeObj = contentBox.chromeObj = chromeBox.chromeObj; + + // First, a whole bunch of basic sanity checks, to ensure that JavaScript + // evaluated in various scopes really does see the world the way this + // test expects it to. + + // The objects appear as global variables in the sandbox, and as + // the sandbox object's properties in chrome. + do_check_true(Components.utils.evalInSandbox("mainObj", contentBox) + === contentBox.mainObj); + do_check_true(Components.utils.evalInSandbox("contentObj", contentBox) + === contentBox.contentObj); + do_check_true(Components.utils.evalInSandbox("chromeObj", contentBox) + === contentBox.chromeObj); + do_check_true(Components.utils.evalInSandbox("mainObj", chromeBox) + === chromeBox.mainObj); + do_check_true(Components.utils.evalInSandbox("contentObj", chromeBox) + === chromeBox.contentObj); + do_check_true(Components.utils.evalInSandbox("chromeObj", chromeBox) + === chromeBox.chromeObj); + + // We (the main global) can see properties of all objects in all globals. + do_check_true(contentBox.mainObj.name === "mainObj"); + do_check_true(contentBox.contentObj.name === "contentObj"); + do_check_true(contentBox.chromeObj.name === "chromeObj"); + + // chromeBox can see properties of all objects in all globals. + do_check_eq(Components.utils.evalInSandbox("mainObj.name", chromeBox), + "mainObj"); + do_check_eq(Components.utils.evalInSandbox("contentObj.name", chromeBox), + "contentObj"); + do_check_eq(Components.utils.evalInSandbox("chromeObj.name", chromeBox), + "chromeObj"); + + // contentBox can see properties of the content object, but not of either + // chrome object, because by default, content -> chrome wrappers hide all + // object properties. + do_check_eq(Components.utils.evalInSandbox("mainObj.name", contentBox), + undefined); + do_check_eq(Components.utils.evalInSandbox("contentObj.name", contentBox), + "contentObj"); + do_check_eq(Components.utils.evalInSandbox("chromeObj.name", contentBox), + undefined); + + // When viewing an object in compartment A from the vantage point of + // compartment B, Debugger should give the same results as debuggee code + // would. + + // Create a debugger, debugging our two sandboxes. + let dbg = new Debugger; + + // Create Debugger.Object instances referring to the two sandboxes, as + // seen from their own compartments. + let contentBoxDO = dbg.addDebuggee(contentBox); + let chromeBoxDO = dbg.addDebuggee(chromeBox); + + // Use Debugger to view the objects from contentBox. We should get the + // same D.O instance from both getProperty and makeDebuggeeValue, and the + // same property visibility we checked for above. + let mainFromContentDO = contentBoxDO.getProperty("mainObj"); + do_check_eq(mainFromContentDO, contentBoxDO.makeDebuggeeValue(mainObj)); + do_check_eq(mainFromContentDO.getProperty("name"), undefined); + do_check_eq(mainFromContentDO.unsafeDereference(), mainObj); + + let contentFromContentDO = contentBoxDO.getProperty("contentObj"); + do_check_eq(contentFromContentDO, contentBoxDO.makeDebuggeeValue(contentObj)); + do_check_eq(contentFromContentDO.getProperty("name"), "contentObj"); + do_check_eq(contentFromContentDO.unsafeDereference(), contentObj); + + let chromeFromContentDO = contentBoxDO.getProperty("chromeObj"); + do_check_eq(chromeFromContentDO, contentBoxDO.makeDebuggeeValue(chromeObj)); + do_check_eq(chromeFromContentDO.getProperty("name"), undefined); + do_check_eq(chromeFromContentDO.unsafeDereference(), chromeObj); + + // Similarly, viewing from chromeBox. + let mainFromChromeDO = chromeBoxDO.getProperty("mainObj"); + do_check_eq(mainFromChromeDO, chromeBoxDO.makeDebuggeeValue(mainObj)); + do_check_eq(mainFromChromeDO.getProperty("name"), "mainObj"); + do_check_eq(mainFromChromeDO.unsafeDereference(), mainObj); + + let contentFromChromeDO = chromeBoxDO.getProperty("contentObj"); + do_check_eq(contentFromChromeDO, chromeBoxDO.makeDebuggeeValue(contentObj)); + do_check_eq(contentFromChromeDO.getProperty("name"), "contentObj"); + do_check_eq(contentFromChromeDO.unsafeDereference(), contentObj); + + let chromeFromChromeDO = chromeBoxDO.getProperty("chromeObj"); + do_check_eq(chromeFromChromeDO, chromeBoxDO.makeDebuggeeValue(chromeObj)); + do_check_eq(chromeFromChromeDO.getProperty("name"), "chromeObj"); + do_check_eq(chromeFromChromeDO.unsafeDereference(), chromeObj); +} diff --git a/devtools/server/tests/unit/test_xpcshell_debugging.js b/devtools/server/tests/unit/test_xpcshell_debugging.js new file mode 100644 index 000000000..7026a02b3 --- /dev/null +++ b/devtools/server/tests/unit/test_xpcshell_debugging.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the xpcshell-test debug support. Ideally we should have this test +// next to the xpcshell support code, but that's tricky... + +function run_test() { + let testFile = do_get_file("xpcshell_debugging_script.js"); + + // _setupDebuggerServer is from xpcshell-test's head.js + let testResumed = false; + let DebuggerServer = _setupDebuggerServer([testFile.path], () => testResumed = true); + let transport = DebuggerServer.connectPipe(); + let client = new DebuggerClient(transport); + client.connect().then(() => { + // Even though we have no tabs, listTabs gives us the chromeDebugger. + client.getProcess().then(response => { + let actor = response.form.actor; + client.attachTab(actor, (response, tabClient) => { + tabClient.attachThread(null, (response, threadClient) => { + threadClient.addOneTimeListener("paused", (event, packet) => { + equal(packet.why.type, "breakpoint", + "yay - hit the breakpoint at the first line in our script"); + // Resume again - next stop should be our "debugger" statement. + threadClient.addOneTimeListener("paused", (event, packet) => { + equal(packet.why.type, "debuggerStatement", + "yay - hit the 'debugger' statement in our script"); + threadClient.resume(() => { + finishClient(client); + }); + }); + threadClient.resume(); + }); + // tell the thread to do the initial resume. This would cause the + // xpcshell test harness to resume and load the file under test. + threadClient.resume(response => { + // should have been told to resume the test itself. + ok(testResumed); + // Now load our test script. + load(testFile.path); + // and our "paused" listener above should get hit. + }); + }); + }); + }); + }); + do_test_pending(); +} diff --git a/devtools/server/tests/unit/testactors.js b/devtools/server/tests/unit/testactors.js new file mode 100644 index 000000000..39564eeef --- /dev/null +++ b/devtools/server/tests/unit/testactors.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common"); +const { RootActor } = require("devtools/server/actors/root"); +const { ThreadActor } = require("devtools/server/actors/script"); +const { DebuggerServer } = require("devtools/server/main"); +const { TabSources } = require("devtools/server/actors/utils/TabSources"); +const promise = require("promise"); +const makeDebugger = require("devtools/server/actors/utils/make-debugger"); + +var gTestGlobals = []; +DebuggerServer.addTestGlobal = function (aGlobal) { + gTestGlobals.push(aGlobal); +}; + +DebuggerServer.getTestGlobal = function (name) { + for (let g of gTestGlobals) { + if (g.__name == name) { + return g; + } + } + + return null; +}; + +// A mock tab list, for use by tests. This simply presents each global in +// gTestGlobals as a tab, and the list is fixed: it never calls its +// onListChanged handler. +// +// As implemented now, we consult gTestGlobals when we're constructed, not +// when we're iterated over, so tests have to add their globals before the +// root actor is created. +function TestTabList(aConnection) { + this.conn = aConnection; + + // An array of actors for each global added with + // DebuggerServer.addTestGlobal. + this._tabActors = []; + + // A pool mapping those actors' names to the actors. + this._tabActorPool = new ActorPool(aConnection); + + for (let global of gTestGlobals) { + let actor = new TestTabActor(aConnection, global); + actor.selected = false; + this._tabActors.push(actor); + this._tabActorPool.addActor(actor); + } + if (this._tabActors.length > 0) { + this._tabActors[0].selected = true; + } + + aConnection.addActorPool(this._tabActorPool); +} + +TestTabList.prototype = { + constructor: TestTabList, + getList: function () { + return Promise.resolve([...this._tabActors]); + } +}; + +function createRootActor(aConnection) +{ + let root = new RootActor(aConnection, { + tabList: new TestTabList(aConnection), + globalActorFactories: DebuggerServer.globalActorFactories, + }); + + root.applicationType = "xpcshell-tests"; + return root; +} + +function TestTabActor(aConnection, aGlobal) +{ + this.conn = aConnection; + this._global = aGlobal; + this._global.wrappedJSObject = aGlobal; + this.threadActor = new ThreadActor(this, this._global); + this.conn.addActor(this.threadActor); + this._attached = false; + this._extraActors = {}; + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: () => [this._global], + shouldAddNewGlobalAsDebuggee: g => g.hostAnnotations && + g.hostAnnotations.type == "document" && + g.hostAnnotations.element === this._global + + }); +} + +TestTabActor.prototype = { + constructor: TestTabActor, + actorPrefix: "TestTabActor", + + get window() { + return this._global; + }, + + get url() { + return this._global.__name; + }, + + get sources() { + if (!this._sources) { + this._sources = new TabSources(this.threadActor); + } + return this._sources; + }, + + form: function () { + let response = { actor: this.actorID, title: this._global.__name }; + + // Walk over tab actors added by extensions and add them to a new ActorPool. + let actorPool = new ActorPool(this.conn); + this._createExtraActors(DebuggerServer.tabActorFactories, actorPool); + if (!actorPool.isEmpty()) { + this._tabActorPool = actorPool; + this.conn.addActorPool(this._tabActorPool); + } + + this._appendExtraActors(response); + + return response; + }, + + onAttach: function (aRequest) { + this._attached = true; + + let response = { type: "tabAttached", threadActor: this.threadActor.actorID }; + this._appendExtraActors(response); + + return response; + }, + + onDetach: function (aRequest) { + if (!this._attached) { + return { "error":"wrongState" }; + } + return { type: "detached" }; + }, + + onReload: function (aRequest) { + this.sources.reset({ sourceMaps: true }); + this.threadActor.clearDebuggees(); + this.threadActor.dbg.addDebuggees(); + return {}; + }, + + removeActorByName: function (aName) { + const actor = this._extraActors[aName]; + if (this._tabActorPool) { + this._tabActorPool.removeActor(actor); + } + delete this._extraActors[aName]; + }, + + /* Support for DebuggerServer.addTabActor. */ + _createExtraActors: createExtraActors, + _appendExtraActors: appendExtraActors +}; + +TestTabActor.prototype.requestTypes = { + "attach": TestTabActor.prototype.onAttach, + "detach": TestTabActor.prototype.onDetach, + "reload": TestTabActor.prototype.onReload +}; + +exports.register = function (handle) { + handle.setRootActor(createRootActor); +}; + +exports.unregister = function (handle) { + handle.setRootActor(null); +}; diff --git a/devtools/server/tests/unit/tracerlocations.js b/devtools/server/tests/unit/tracerlocations.js new file mode 100644 index 000000000..aa4677c0f --- /dev/null +++ b/devtools/server/tests/unit/tracerlocations.js @@ -0,0 +1,8 @@ +// For JS Tracer tests dealing with source locations. + +function foo(x) { + x += 6; + return "bar"; +} + +foo(42); diff --git a/devtools/server/tests/unit/xpcshell.ini b/devtools/server/tests/unit/xpcshell.ini new file mode 100644 index 000000000..703aa48fb --- /dev/null +++ b/devtools/server/tests/unit/xpcshell.ini @@ -0,0 +1,232 @@ +[DEFAULT] +tags = devtools +head = head_dbg.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' + +support-files = + babel_and_browserify_script_with_source_map.js + source-map-data/sourcemapped.coffee + source-map-data/sourcemapped.map + post_init_global_actors.js + post_init_tab_actors.js + pre_init_global_actors.js + pre_init_tab_actors.js + registertestactors-01.js + registertestactors-02.js + registertestactors-03.js + sourcemapped.js + testactors.js + hello-actor.js + setBreakpoint-on-column.js + setBreakpoint-on-column-in-gcd-script.js + setBreakpoint-on-column-with-no-offsets.js + setBreakpoint-on-column-with-no-offsets-at-end-of-line.js + setBreakpoint-on-column-with-no-offsets-in-gcd-script.js + setBreakpoint-on-line.js + setBreakpoint-on-line-in-gcd-script.js + setBreakpoint-on-line-with-multiple-offsets.js + setBreakpoint-on-line-with-multiple-statements.js + setBreakpoint-on-line-with-no-offsets.js + setBreakpoint-on-line-with-no-offsets-in-gcd-script.js + addons/web-extension/manifest.json + addons/web-extension-upgrade/manifest.json + addons/web-extension2/manifest.json + +[test_addon_reload.js] +[test_addons_actor.js] +[test_animation_name.js] +[test_animation_type.js] +[test_actor-registry-actor.js] +[test_nesting-01.js] +[test_nesting-02.js] +[test_nesting-03.js] +[test_forwardingprefix.js] +[test_getyoungestframe.js] +[test_nsjsinspector.js] +[test_dbgactor.js] +[test_dbgglobal.js] +[test_dbgclient_debuggerstatement.js] +[test_attach.js] +[test_MemoryActor_saveHeapSnapshot_01.js] +[test_MemoryActor_saveHeapSnapshot_02.js] +[test_MemoryActor_saveHeapSnapshot_03.js] +[test_reattach-thread.js] +[test_blackboxing-01.js] +[test_blackboxing-02.js] +[test_blackboxing-03.js] +[test_blackboxing-04.js] +[test_blackboxing-05.js] +[test_blackboxing-06.js] +[test_blackboxing-07.js] +[test_frameactor-01.js] +[test_frameactor-02.js] +[test_frameactor-03.js] +[test_frameactor-04.js] +[test_frameactor-05.js] +[test_framearguments-01.js] +[test_getRuleText.js] +[test_getTextAtLineColumn.js] +[test_pauselifetime-01.js] +[test_pauselifetime-02.js] +[test_pauselifetime-03.js] +[test_pauselifetime-04.js] +[test_threadlifetime-01.js] +[test_threadlifetime-02.js] +[test_threadlifetime-03.js] +[test_threadlifetime-04.js] +[test_threadlifetime-05.js] +[test_threadlifetime-06.js] +[test_functiongrips-01.js] +[test_frameclient-01.js] +[test_frameclient-02.js] +[test_nativewrappers.js] +[test_nodelistactor.js] +[test_eval-01.js] +[test_eval-02.js] +[test_eval-03.js] +[test_eval-04.js] +[test_eval-05.js] +[test_promises_actor_attach.js] +[test_promises_actor_exist.js] +[test_promises_actor_list_promises.js] +[test_promises_actor_onnewpromise.js] +[test_promises_actor_onpromisesettled.js] +[test_promises_client_getdependentpromises.js] +[test_promises_object_creationtimestamp.js] +[test_promises_object_timetosettle-01.js] +[test_promises_object_timetosettle-02.js] +[test_protocol_abort.js] +[test_protocol_async.js] +[test_protocol_children.js] +[test_protocol_formtype.js] +[test_protocol_longstring.js] +[test_protocol_simple.js] +[test_protocol_stack.js] +[test_protocol_unregister.js] +[test_breakpoint-01.js] +[test_register_actor.js] +[test_breakpoint-02.js] +[test_breakpoint-03.js] +[test_breakpoint-04.js] +[test_breakpoint-05.js] +[test_breakpoint-06.js] +[test_breakpoint-07.js] +[test_breakpoint-08.js] +[test_breakpoint-09.js] +[test_breakpoint-10.js] +[test_breakpoint-11.js] +[test_breakpoint-12.js] +[test_breakpoint-13.js] +[test_breakpoint-14.js] +[test_breakpoint-15.js] +[test_breakpoint-16.js] +[test_breakpoint-17.js] +[test_breakpoint-18.js] +[test_breakpoint-19.js] +skip-if = true +reason = bug 1104838 +[test_breakpoint-20.js] +[test_breakpoint-21.js] +[test_conditional_breakpoint-01.js] +[test_conditional_breakpoint-02.js] +[test_conditional_breakpoint-03.js] +[test_eventlooplag_actor.js] +skip-if = true +reason = only ran on B2G +[test_listsources-01.js] +[test_listsources-02.js] +[test_listsources-03.js] +[test_listsources-04.js] +[test_new_source-01.js] +[test_sourcemaps-01.js] +[test_sourcemaps-02.js] +[test_sourcemaps-03.js] +[test_sourcemaps-04.js] +[test_sourcemaps-05.js] +[test_sourcemaps-06.js] +[test_sourcemaps-07.js] +[test_sourcemaps-08.js] +[test_sourcemaps-09.js] +[test_sourcemaps-10.js] +[test_sourcemaps-11.js] +[test_sourcemaps-12.js] +[test_sourcemaps-13.js] +[test_sourcemaps-16.js] +[test_sourcemaps-17.js] +[test_objectgrips-01.js] +[test_objectgrips-02.js] +[test_objectgrips-03.js] +[test_objectgrips-04.js] +[test_objectgrips-05.js] +[test_objectgrips-06.js] +[test_objectgrips-07.js] +[test_objectgrips-08.js] +[test_objectgrips-09.js] +[test_objectgrips-10.js] +[test_objectgrips-11.js] +[test_objectgrips-12.js] +[test_objectgrips-13.js] +[test_promise_state-01.js] +[test_promise_state-02.js] +[test_promise_state-03.js] +[test_interrupt.js] +[test_stepping-01.js] +[test_stepping-02.js] +[test_stepping-03.js] +[test_stepping-04.js] +[test_stepping-05.js] +[test_stepping-06.js] +[test_stepping-07.js] +[test_framebindings-01.js] +[test_framebindings-02.js] +[test_framebindings-03.js] +[test_framebindings-04.js] +[test_framebindings-05.js] +[test_framebindings-06.js] +[test_framebindings-07.js] +[test_pause_exceptions-01.js] +[test_pause_exceptions-02.js] +[test_longstringactor.js] +[test_longstringgrips-01.js] +[test_longstringgrips-02.js] +[test_source-01.js] +[test_breakpoint-actor-map.js] +[test_profiler_activation-01.js] +[test_profiler_activation-02.js] +[test_profiler_close.js] +[test_profiler_data.js] +[test_profiler_events-01.js] +[test_profiler_events-02.js] +[test_profiler_getbufferinfo.js] +[test_profiler_getfeatures.js] +[test_profiler_getsharedlibraryinformation.js] +[test_unsafeDereference.js] +[test_add_actors.js] +[test_ignore_caught_exceptions.js] +[test_ignore_no_interface_exceptions.js] +[test_requestTypes.js] +reason = bug 937197 +[test_layout-reflows-observer.js] +[test_protocolSpec.js] +[test_registerClient.js] +[test_client_request.js] +[test_monitor_actor.js] +[test_symbols-01.js] +[test_symbols-02.js] +[test_get-executable-lines.js] +[test_get-executable-lines-source-map.js] +[test_xpcshell_debugging.js] +support-files = xpcshell_debugging_script.js +[test_setBreakpoint-on-column.js] +[test_setBreakpoint-on-column-in-gcd-script.js] +[test_setBreakpoint-on-column-with-no-offsets-at-end-of-line.js] +[test_setBreakpoint-on-line.js] +[test_setBreakpoint-on-line-in-gcd-script.js] +[test_setBreakpoint-on-line-with-multiple-offsets.js] +[test_setBreakpoint-on-line-with-multiple-statements.js] +[test_setBreakpoint-on-line-with-no-offsets.js] +[test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js] +[test_safe-getter.js] +[test_client_close.js] diff --git a/devtools/server/tests/unit/xpcshell_debugging_script.js b/devtools/server/tests/unit/xpcshell_debugging_script.js new file mode 100644 index 000000000..de2870e96 --- /dev/null +++ b/devtools/server/tests/unit/xpcshell_debugging_script.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This is a file that test_xpcshell_debugging.js debugs. + +// We should hit this dump as it is the first debuggable line +dump("hello from the debugee!\n"); + +debugger; // and why not check we hit this!? diff --git a/devtools/server/websocket-server.js b/devtools/server/websocket-server.js new file mode 100644 index 000000000..6e8a80fec --- /dev/null +++ b/devtools/server/websocket-server.js @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, CC } = require("chrome"); +const Promise = require("promise"); +const { Task } = require("devtools/shared/task"); +const { executeSoon } = require("devtools/shared/DevToolsUtils"); +const { delimitedRead } = require("devtools/shared/transport/stream-utils"); +const CryptoHash = CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString"); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + +// Limit the header size to put an upper bound on allocated memory +const HEADER_MAX_LEN = 8000; + +/** + * Read a line from async input stream and return promise that resolves to the line once + * it has been read. If the line is longer than HEADER_MAX_LEN, will throw error. + */ +function readLine(input) { + return new Promise((resolve, reject) => { + let line = ""; + let wait = () => { + input.asyncWait(stream => { + try { + let amountToRead = HEADER_MAX_LEN - line.length; + line += delimitedRead(input, "\n", amountToRead); + + if (line.endsWith("\n")) { + resolve(line.trimRight()); + return; + } + + if (line.length >= HEADER_MAX_LEN) { + throw new Error( + `Failed to read HTTP header longer than ${HEADER_MAX_LEN} bytes`); + } + + wait(); + } catch (ex) { + reject(ex); + } + }, 0, 0, threadManager.currentThread); + }; + + wait(); + }); +} + +/** + * Write a string of bytes to async output stream and return promise that resolves once + * all data has been written. Doesn't do any utf-16/utf-8 conversion - the string is + * treated as an array of bytes. + */ +function writeString(output, data) { + return new Promise((resolve, reject) => { + let wait = () => { + if (data.length === 0) { + resolve(); + return; + } + + output.asyncWait(stream => { + try { + let written = output.write(data, data.length); + data = data.slice(written); + wait(); + } catch (ex) { + reject(ex); + } + }, 0, 0, threadManager.currentThread); + }; + + wait(); + }); +} + +/** + * Read HTTP request from async input stream. + * @return Request line (string) and Map of header names and values. + */ +const readHttpRequest = Task.async(function* (input) { + let requestLine = ""; + let headers = new Map(); + + while (true) { + let line = yield readLine(input); + if (line.length == 0) { + break; + } + + if (!requestLine) { + requestLine = line; + } else { + let colon = line.indexOf(":"); + if (colon == -1) { + throw new Error(`Malformed HTTP header: ${line}`); + } + + let name = line.slice(0, colon).toLowerCase(); + let value = line.slice(colon + 1).trim(); + headers.set(name, value); + } + } + + return { requestLine, headers }; +}); + +/** + * Write HTTP response (array of strings) to async output stream. + */ +function writeHttpResponse(output, response) { + let responseString = response.join("\r\n") + "\r\n\r\n"; + return writeString(output, responseString); +} + +/** + * Process the WebSocket handshake headers and return the key to be sent in + * Sec-WebSocket-Accept response header. + */ +function processRequest({ requestLine, headers }) { + let [ method, path ] = requestLine.split(" "); + if (method !== "GET") { + throw new Error("The handshake request must use GET method"); + } + + if (path !== "/") { + throw new Error("The handshake request has unknown path"); + } + + let upgrade = headers.get("upgrade"); + if (!upgrade || upgrade !== "websocket") { + throw new Error("The handshake request has incorrect Upgrade header"); + } + + let connection = headers.get("connection"); + if (!connection || !connection.split(",").map(t => t.trim()).includes("Upgrade")) { + throw new Error("The handshake request has incorrect Connection header"); + } + + let version = headers.get("sec-websocket-version"); + if (!version || version !== "13") { + throw new Error("The handshake request must have Sec-WebSocket-Version: 13"); + } + + // Compute the accept key + let key = headers.get("sec-websocket-key"); + if (!key) { + throw new Error("The handshake request must have a Sec-WebSocket-Key header"); + } + + return { acceptKey: computeKey(key) }; +} + +function computeKey(key) { + let str = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + let data = Array.from(str, ch => ch.charCodeAt(0)); + let hash = new CryptoHash("sha1"); + hash.update(data, data.length); + return hash.finish(true); +} + +/** + * Perform the server part of a WebSocket opening handshake on an incoming connection. + */ +const serverHandshake = Task.async(function* (input, output) { + // Read the request + let request = yield readHttpRequest(input); + + try { + // Check and extract info from the request + let { acceptKey } = processRequest(request); + + // Send response headers + yield writeHttpResponse(output, [ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Accept: ${acceptKey}`, + ]); + } catch (error) { + // Send error response in case of error + yield writeHttpResponse(output, [ "HTTP/1.1 400 Bad Request" ]); + throw error; + } +}); + +/** + * Accept an incoming WebSocket server connection. + * Takes an established nsISocketTransport in the parameters. + * Performs the WebSocket handshake and waits for the WebSocket to open. + * Returns Promise with a WebSocket ready to send and receive messages. + */ +const accept = Task.async(function* (transport, input, output) { + yield serverHandshake(input, output); + + let transportProvider = { + setListener(upgradeListener) { + // The onTransportAvailable callback shouldn't be called synchronously. + executeSoon(() => { + upgradeListener.onTransportAvailable(transport, input, output); + }); + } + }; + + return new Promise((resolve, reject) => { + let socket = WebSocket.createServerWebSocket(null, [], transportProvider, ""); + socket.addEventListener("close", () => { + input.close(); + output.close(); + }); + + socket.onopen = () => resolve(socket); + socket.onerror = err => reject(err); + }); +}); + +exports.accept = accept; diff --git a/devtools/server/worker.js b/devtools/server/worker.js new file mode 100644 index 000000000..e9a1afb8e --- /dev/null +++ b/devtools/server/worker.js @@ -0,0 +1,110 @@ +"use strict"; + +// This function is used to do remote procedure calls from the worker to the +// main thread. It is exposed as a built-in global to every module by the +// worker loader. To make sure the worker loader can access it, it needs to be +// defined before loading the worker loader script below. +this.rpc = function (method, ...params) { + let id = nextId++; + + postMessage(JSON.stringify({ + type: "rpc", + method: method, + params: params, + id: id + })); + + let deferred = Promise.defer(); + rpcDeferreds[id] = deferred; + return deferred.promise; +}; + +loadSubScript("resource://devtools/shared/worker/loader.js"); + +var Promise = worker.require("promise"); +var { ActorPool } = worker.require("devtools/server/actors/common"); +var { ThreadActor } = worker.require("devtools/server/actors/script"); +var { WebConsoleActor } = worker.require("devtools/server/actors/webconsole"); +var { TabSources } = worker.require("devtools/server/actors/utils/TabSources"); +var makeDebugger = worker.require("devtools/server/actors/utils/make-debugger"); +var { DebuggerServer } = worker.require("devtools/server/main"); + +DebuggerServer.init(); +DebuggerServer.createRootActor = function () { + throw new Error("Should never get here!"); +}; + +var connections = Object.create(null); +var nextId = 0; +var rpcDeferreds = []; + +this.addEventListener("message", function (event) { + let packet = JSON.parse(event.data); + switch (packet.type) { + case "connect": + // Step 3: Create a connection to the parent. + let connection = DebuggerServer.connectToParent(packet.id, this); + connections[packet.id] = { + connection : connection, + rpcs: [] + }; + + // Step 4: Create a thread actor for the connection to the parent. + let pool = new ActorPool(connection); + connection.addActorPool(pool); + + let sources = null; + + let parent = { + actorID: packet.id, + + makeDebugger: makeDebugger.bind(null, { + findDebuggees: () => { + return [this.global]; + }, + + shouldAddNewGlobalAsDebuggee: () => { + return true; + }, + }), + + get sources() { + if (sources === null) { + sources = new TabSources(threadActor); + } + return sources; + }, + + window: global + }; + + let threadActor = new ThreadActor(parent, global); + pool.addActor(threadActor); + + let consoleActor = new WebConsoleActor(connection, parent); + pool.addActor(consoleActor); + + // Step 5: Send a response packet to the parent to notify + // it that a connection has been established. + postMessage(JSON.stringify({ + type: "connected", + id: packet.id, + threadActor: threadActor.actorID, + consoleActor: consoleActor.actorID, + })); + break; + + case "disconnect": + connections[packet.id].connection.close(); + break; + + case "rpc": + let deferred = rpcDeferreds[packet.id]; + delete rpcDeferreds[packet.id]; + if (packet.error) { + deferred.reject(packet.error); + } + deferred.resolve(packet.result); + break; + } +}); -- cgit v1.2.3